From 37c4a3aa36d18e55e9a8b11f78254c1201feba0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:13:05 +0000 Subject: [PATCH 001/180] build(deps): bump SonarSource/sonarqube-scan-action Bumps [SonarSource/sonarqube-scan-action](https://github.com/sonarsource/sonarqube-scan-action) from 5 to 6. - [Release notes](https://github.com/sonarsource/sonarqube-scan-action/releases) - [Commits](https://github.com/sonarsource/sonarqube-scan-action/compare/v5...v6) --- updated-dependencies: - dependency-name: SonarSource/sonarqube-scan-action dependency-version: '6' dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- .github/workflows/ci-sonar.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-sonar.yml b/.github/workflows/ci-sonar.yml index a06986dfcd..4715aa1bd5 100644 --- a/.github/workflows/ci-sonar.yml +++ b/.github/workflows/ci-sonar.yml @@ -14,6 +14,6 @@ jobs: with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: SonarQube Scan - uses: SonarSource/sonarqube-scan-action@v5 + uses: SonarSource/sonarqube-scan-action@v6 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From de2777819269eeef020126878c5f45f510cb2ec7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 21:19:35 +0000 Subject: [PATCH 002/180] Bump next in /nym-node-status-api/nym-node-status-ui Bumps [next](https://github.com/vercel/next.js) from 15.4.1 to 15.4.7. - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/compare/v15.4.1...v15.4.7) --- updated-dependencies: - dependency-name: next dependency-version: 15.4.7 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- .../nym-node-status-ui/package.json | 2 +- .../nym-node-status-ui/pnpm-lock.yaml | 408 +++++++++--------- 2 files changed, 195 insertions(+), 215 deletions(-) diff --git a/nym-node-status-api/nym-node-status-ui/package.json b/nym-node-status-api/nym-node-status-ui/package.json index 610a288446..a1e23b9be0 100644 --- a/nym-node-status-api/nym-node-status-ui/package.json +++ b/nym-node-status-api/nym-node-status-ui/package.json @@ -21,7 +21,7 @@ "@tanstack/react-query": "^5.83.0", "dayjs": "^1.11.13", "material-react-table": "^3.2.1", - "next": "^15.4.1", + "next": "^15.4.7", "openapi-typescript": "^7.5.2", "react": "^19.0.0", "react-country-flag": "^3.1.0", diff --git a/nym-node-status-api/nym-node-status-ui/pnpm-lock.yaml b/nym-node-status-api/nym-node-status-ui/pnpm-lock.yaml index ab4a48acac..fa5b685a13 100644 --- a/nym-node-status-api/nym-node-status-ui/pnpm-lock.yaml +++ b/nym-node-status-api/nym-node-status-ui/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: version: 7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mui/material-nextjs': specifier: ^7.2.0 - version: 7.2.0(@emotion/cache@11.14.0)(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(next@15.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) + version: 7.2.0(@emotion/cache@11.14.0)(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(next@15.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) '@mui/x-charts': specifier: ^8.8.0 version: 8.8.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@mui/material@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -39,8 +39,8 @@ importers: specifier: ^3.2.1 version: 3.2.1(e4be24f02074e47c0e16001efb50991b) next: - specifier: ^15.4.1 - version: 15.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^15.4.7 + version: 15.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0) openapi-typescript: specifier: ^7.5.2 version: 7.5.2(typescript@5.7.3) @@ -173,8 +173,8 @@ packages: cpu: [x64] os: [win32] - '@emnapi/runtime@1.4.4': - resolution: {integrity: sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==} + '@emnapi/runtime@1.7.0': + resolution: {integrity: sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==} '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -230,124 +230,139 @@ packages: '@emotion/weak-memoize@0.4.0': resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} - '@img/sharp-darwin-arm64@0.34.3': - resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.3': - resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.0': - resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.0': - resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.0': - resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.2.0': - resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - '@img/sharp-libvips-linux-ppc64@1.2.0': - resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==} + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - '@img/sharp-libvips-linux-s390x@1.2.0': - resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.2.0': - resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.2.0': - resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.2.0': - resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.34.3': - resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.34.3': - resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - '@img/sharp-linux-ppc64@0.34.3': - resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==} + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - '@img/sharp-linux-s390x@0.34.3': - resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.34.3': - resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.34.3': - resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.34.3': - resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.34.3': - resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.3': - resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==} + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.3': - resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.3': - resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] @@ -551,53 +566,53 @@ packages: '@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 - '@next/env@15.4.1': - resolution: {integrity: sha512-DXQwFGAE2VH+f2TJsKepRXpODPU+scf5fDbKOME8MMyeyswe4XwgRdiiIYmBfkXU+2ssliLYznajTrOQdnLR5A==} + '@next/env@15.4.7': + resolution: {integrity: sha512-PrBIpO8oljZGTOe9HH0miix1w5MUiGJ/q83Jge03mHEE0E3pyqzAy2+l5G6aJDbXoobmxPJTVhbCuwlLtjSHwg==} - '@next/swc-darwin-arm64@15.4.1': - resolution: {integrity: sha512-L+81yMsiHq82VRXS2RVq6OgDwjvA4kDksGU8hfiDHEXP+ncKIUhUsadAVB+MRIp2FErs/5hpXR0u2eluWPAhig==} + '@next/swc-darwin-arm64@15.4.7': + resolution: {integrity: sha512-2Dkb+VUTp9kHHkSqtws4fDl2Oxms29HcZBwFIda1X7Ztudzy7M6XF9HDS2dq85TmdN47VpuhjE+i6wgnIboVzQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.4.1': - resolution: {integrity: sha512-jfz1RXu6SzL14lFl05/MNkcN35lTLMJWPbqt7Xaj35+ZWAX342aePIJrN6xBdGeKl6jPXJm0Yqo3Xvh3Gpo3Uw==} + '@next/swc-darwin-x64@15.4.7': + resolution: {integrity: sha512-qaMnEozKdWezlmh1OGDVFueFv2z9lWTcLvt7e39QA3YOvZHNpN2rLs/IQLwZaUiw2jSvxW07LxMCWtOqsWFNQg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.4.1': - resolution: {integrity: sha512-k0tOFn3dsnkaGfs6iQz8Ms6f1CyQe4GacXF979sL8PNQxjYS1swx9VsOyUQYaPoGV8nAZ7OX8cYaeiXGq9ahPQ==} + '@next/swc-linux-arm64-gnu@15.4.7': + resolution: {integrity: sha512-ny7lODPE7a15Qms8LZiN9wjNWIeI+iAZOFDOnv2pcHStncUr7cr9lD5XF81mdhrBXLUP9yT9RzlmSWKIazWoDw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.4.1': - resolution: {integrity: sha512-4ogGQ/3qDzbbK3IwV88ltihHFbQVq6Qr+uEapzXHXBH1KsVBZOB50sn6BWHPcFjwSoMX2Tj9eH/fZvQnSIgc3g==} + '@next/swc-linux-arm64-musl@15.4.7': + resolution: {integrity: sha512-4SaCjlFR/2hGJqZLLWycccy1t+wBrE/vyJWnYaZJhUVHccpGLG5q0C+Xkw4iRzUIkE+/dr90MJRUym3s1+vO8A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.4.1': - resolution: {integrity: sha512-Jj0Rfw3wIgp+eahMz/tOGwlcYYEFjlBPKU7NqoOkTX0LY45i5W0WcDpgiDWSLrN8KFQq/LW7fZq46gxGCiOYlQ==} + '@next/swc-linux-x64-gnu@15.4.7': + resolution: {integrity: sha512-2uNXjxvONyRidg00VwvlTYDwC9EgCGNzPAPYbttIATZRxmOZ3hllk/YYESzHZb65eyZfBR5g9xgCZjRAl9YYGg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.4.1': - resolution: {integrity: sha512-9WlEZfnw1vFqkWsTMzZDgNL7AUI1aiBHi0S2m8jvycPyCq/fbZjtE/nDkhJRYbSjXbtRHYLDBlmP95kpjEmJbw==} + '@next/swc-linux-x64-musl@15.4.7': + resolution: {integrity: sha512-ceNbPjsFgLscYNGKSu4I6LYaadq2B8tcK116nVuInpHHdAWLWSwVK6CHNvCi0wVS9+TTArIFKJGsEyVD1H+4Kg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.4.1': - resolution: {integrity: sha512-WodRbZ9g6CQLRZsG3gtrA9w7Qfa9BwDzhFVdlI6sV0OCPq9JrOrJSp9/ioLsezbV8w9RCJ8v55uzJuJ5RgWLZg==} + '@next/swc-win32-arm64-msvc@15.4.7': + resolution: {integrity: sha512-pZyxmY1iHlZJ04LUL7Css8bNvsYAMYOY9JRwFA3HZgpaNKsJSowD09Vg2R9734GxAcLJc2KDQHSCR91uD6/AAw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.4.1': - resolution: {integrity: sha512-y+wTBxelk2xiNofmDOVU7O5WxTHcvOoL3srOM0kxTzKDjQ57kPU0tpnPJ/BWrRnsOwXEv0+3QSbGR7hY4n9LkQ==} + '@next/swc-win32-x64-msvc@15.4.7': + resolution: {integrity: sha512-HjuwPJ7BeRzgl3KrjKqD2iDng0eQIpIReyhpF5r4yeAHFwWRuAhfW92rWv/r3qeQHEwHsLRzFDvMqRjyM5DI6A==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -733,8 +748,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001695: - resolution: {integrity: sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==} + caniuse-lite@1.0.30001754: + resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==} change-case@5.4.4: resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} @@ -746,20 +761,6 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} - - color@4.2.3: - resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} - engines: {node: '>=12.5.0'} - colorette@1.4.0: resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} @@ -832,8 +833,8 @@ packages: delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} - detect-libc@2.0.4: - resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} dom-helpers@5.2.1: @@ -889,9 +890,6 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} @@ -944,13 +942,13 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.8: - resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - next@15.4.1: - resolution: {integrity: sha512-eNKB1q8C7o9zXF8+jgJs2CzSLIU3T6bQtX6DcTnCq1sIR1CJ0GlSyRs1BubQi3/JgCnr9Vr+rS5mOMI38FFyQw==} + next@15.4.7: + resolution: {integrity: sha512-OcqRugwF7n7mC8OSYjvsZhhG1AYSvulor1EIUsIkbbEbf1qoE5EbH36Swj8WhF4cHqmDgkiam3z1c1W0J1Wifg==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -1077,18 +1075,15 @@ packages: scheduler@0.25.0: resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true - sharp@0.34.3: - resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - simple-swizzle@0.2.2: - resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1259,7 +1254,7 @@ snapshots: '@biomejs/cli-win32-x64@1.9.4': optional: true - '@emnapi/runtime@1.4.4': + '@emnapi/runtime@1.7.0': dependencies: tslib: 2.8.1 optional: true @@ -1347,90 +1342,101 @@ snapshots: '@emotion/weak-memoize@0.4.0': {} - '@img/sharp-darwin-arm64@0.34.3': + '@img/colour@1.0.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.0 + '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true - '@img/sharp-darwin-x64@0.34.3': + '@img/sharp-darwin-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.0 + '@img/sharp-libvips-darwin-x64': 1.2.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.2.0': + '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true - '@img/sharp-libvips-darwin-x64@1.2.0': + '@img/sharp-libvips-darwin-x64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm64@1.2.0': + '@img/sharp-libvips-linux-arm64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm@1.2.0': + '@img/sharp-libvips-linux-arm@1.2.4': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.0': + '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true - '@img/sharp-libvips-linux-s390x@1.2.0': + '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true - '@img/sharp-libvips-linux-x64@1.2.0': + '@img/sharp-libvips-linux-s390x@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + '@img/sharp-libvips-linux-x64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.0': + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true - '@img/sharp-linux-arm64@0.34.3': + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.0 + '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true - '@img/sharp-linux-arm@0.34.3': + '@img/sharp-linux-arm@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.0 + '@img/sharp-libvips-linux-arm': 1.2.4 optional: true - '@img/sharp-linux-ppc64@0.34.3': + '@img/sharp-linux-ppc64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.0 + '@img/sharp-libvips-linux-ppc64': 1.2.4 optional: true - '@img/sharp-linux-s390x@0.34.3': + '@img/sharp-linux-riscv64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.0 + '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true - '@img/sharp-linux-x64@0.34.3': + '@img/sharp-linux-s390x@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.0 + '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true - '@img/sharp-linuxmusl-arm64@0.34.3': + '@img/sharp-linux-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + '@img/sharp-libvips-linux-x64': 1.2.4 optional: true - '@img/sharp-linuxmusl-x64@0.34.3': + '@img/sharp-linuxmusl-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true - '@img/sharp-wasm32@0.34.3': + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.4.4 + '@emnapi/runtime': 1.7.0 optional: true - '@img/sharp-win32-arm64@0.34.3': + '@img/sharp-win32-arm64@0.34.5': optional: true - '@img/sharp-win32-ia32@0.34.3': + '@img/sharp-win32-ia32@0.34.5': optional: true - '@img/sharp-win32-x64@0.34.3': + '@img/sharp-win32-x64@0.34.5': optional: true '@jridgewell/gen-mapping@0.3.8': @@ -1460,11 +1466,11 @@ snapshots: optionalDependencies: '@types/react': 19.0.7 - '@mui/material-nextjs@7.2.0(@emotion/cache@11.14.0)(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(next@15.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': + '@mui/material-nextjs@7.2.0(@emotion/cache@11.14.0)(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(next@15.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.27.6 '@emotion/react': 11.14.0(@types/react@19.0.7)(react@19.0.0) - next: 15.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 optionalDependencies: '@emotion/cache': 11.14.0 @@ -1631,30 +1637,30 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@next/env@15.4.1': {} + '@next/env@15.4.7': {} - '@next/swc-darwin-arm64@15.4.1': + '@next/swc-darwin-arm64@15.4.7': optional: true - '@next/swc-darwin-x64@15.4.1': + '@next/swc-darwin-x64@15.4.7': optional: true - '@next/swc-linux-arm64-gnu@15.4.1': + '@next/swc-linux-arm64-gnu@15.4.7': optional: true - '@next/swc-linux-arm64-musl@15.4.1': + '@next/swc-linux-arm64-musl@15.4.7': optional: true - '@next/swc-linux-x64-gnu@15.4.1': + '@next/swc-linux-x64-gnu@15.4.7': optional: true - '@next/swc-linux-x64-musl@15.4.1': + '@next/swc-linux-x64-musl@15.4.7': optional: true - '@next/swc-win32-arm64-msvc@15.4.1': + '@next/swc-win32-arm64-msvc@15.4.7': optional: true - '@next/swc-win32-x64-msvc@15.4.1': + '@next/swc-win32-x64-msvc@15.4.7': optional: true '@popperjs/core@2.11.8': {} @@ -1787,7 +1793,7 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001695: {} + caniuse-lite@1.0.30001754: {} change-case@5.4.4: {} @@ -1795,26 +1801,6 @@ snapshots: clsx@2.1.1: {} - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - optional: true - - color-name@1.1.4: - optional: true - - color-string@1.9.1: - dependencies: - color-name: 1.1.4 - simple-swizzle: 0.2.2 - optional: true - - color@4.2.3: - dependencies: - color-convert: 2.0.1 - color-string: 1.9.1 - optional: true - colorette@1.4.0: {} convert-source-map@1.9.0: {} @@ -1881,7 +1867,7 @@ snapshots: dependencies: robust-predicates: 3.0.2 - detect-libc@2.0.4: + detect-libc@2.1.2: optional: true dom-helpers@5.2.1: @@ -1931,9 +1917,6 @@ snapshots: is-arrayish@0.2.1: {} - is-arrayish@0.3.2: - optional: true - is-core-module@2.16.1: dependencies: hasown: 2.0.2 @@ -1978,27 +1961,27 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.8: {} + nanoid@3.3.11: {} - next@15.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next@15.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - '@next/env': 15.4.1 + '@next/env': 15.4.7 '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001695 + caniuse-lite: 1.0.30001754 postcss: 8.4.31 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) styled-jsx: 5.1.6(react@19.0.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.4.1 - '@next/swc-darwin-x64': 15.4.1 - '@next/swc-linux-arm64-gnu': 15.4.1 - '@next/swc-linux-arm64-musl': 15.4.1 - '@next/swc-linux-x64-gnu': 15.4.1 - '@next/swc-linux-x64-musl': 15.4.1 - '@next/swc-win32-arm64-msvc': 15.4.1 - '@next/swc-win32-x64-msvc': 15.4.1 - sharp: 0.34.3 + '@next/swc-darwin-arm64': 15.4.7 + '@next/swc-darwin-x64': 15.4.7 + '@next/swc-linux-arm64-gnu': 15.4.7 + '@next/swc-linux-arm64-musl': 15.4.7 + '@next/swc-linux-x64-gnu': 15.4.7 + '@next/swc-linux-x64-musl': 15.4.7 + '@next/swc-win32-arm64-msvc': 15.4.7 + '@next/swc-win32-x64-msvc': 15.4.7 + sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -2048,7 +2031,7 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.8 + nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -2102,42 +2085,39 @@ snapshots: scheduler@0.25.0: {} - semver@7.7.2: + semver@7.7.3: optional: true - sharp@0.34.3: + sharp@0.34.5: dependencies: - color: 4.2.3 - detect-libc: 2.0.4 - semver: 7.7.2 + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.3 - '@img/sharp-darwin-x64': 0.34.3 - '@img/sharp-libvips-darwin-arm64': 1.2.0 - '@img/sharp-libvips-darwin-x64': 1.2.0 - '@img/sharp-libvips-linux-arm': 1.2.0 - '@img/sharp-libvips-linux-arm64': 1.2.0 - '@img/sharp-libvips-linux-ppc64': 1.2.0 - '@img/sharp-libvips-linux-s390x': 1.2.0 - '@img/sharp-libvips-linux-x64': 1.2.0 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 - '@img/sharp-libvips-linuxmusl-x64': 1.2.0 - '@img/sharp-linux-arm': 0.34.3 - '@img/sharp-linux-arm64': 0.34.3 - '@img/sharp-linux-ppc64': 0.34.3 - '@img/sharp-linux-s390x': 0.34.3 - '@img/sharp-linux-x64': 0.34.3 - '@img/sharp-linuxmusl-arm64': 0.34.3 - '@img/sharp-linuxmusl-x64': 0.34.3 - '@img/sharp-wasm32': 0.34.3 - '@img/sharp-win32-arm64': 0.34.3 - '@img/sharp-win32-ia32': 0.34.3 - '@img/sharp-win32-x64': 0.34.3 - optional: true - - simple-swizzle@0.2.2: - dependencies: - is-arrayish: 0.3.2 + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 optional: true source-map-js@1.2.1: {} From fc5d31093531dc64299cc766397de705fe1deb7e Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Tue, 11 Nov 2025 15:04:09 +0100 Subject: [PATCH 003/180] add QUIC setup script to nym-node-cli --- scripts/nym-node-setup/nym-node-cli.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index ebae150523..6b001df8d2 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -33,6 +33,7 @@ class NodeSetupCLI: self.tunnel_manager_sh = self._check_gwx_mode() and self.fetch_script("network_tunnel_manager.sh") self.wg_ip_tables_manager_sh = self._check_gwx_mode() and self.fetch_script("wireguard-exit-policy-manager.sh") self.wg_ip_tables_test_sh = self._check_gwx_mode() and self.fetch_script("exit-policy-tests.sh") + self.wg_ip_tables_test_sh = self._check_gwx_mode() and self.fetch_script("quic_bridge_deployment.sh") def print_welcome_message(self): """Welcome user, warns for needed pre-reqs and asks for confimation""" @@ -128,7 +129,9 @@ class NodeSetupCLI: "network_tunnel_manager.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/network_tunnel_manager.sh", "wireguard-exit-policy-manager.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh", "exit-policy-tests.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/exit-policy-tests.sh", + "quic_bridge_deployment.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/quic_bridge_deployment.sh" } + return scripts_urls[script_init_name] def run_script( @@ -320,6 +323,10 @@ class NodeSetupCLI: self.run_script(self.wg_ip_tables_manager_sh, args=["status"]) self.run_script(self.wg_ip_tables_test_sh) + def quic_bridge_deploy(self): + """Setup QUIC bridge and configuration using external script""" + self.run_script(self.quic_bridge_deployment_sh, args=["full_bridge_setup"], env=run_env) + return def run_nym_node_as_service(self): """Starts /etc/systemd/system/nym-node.service based on prompt using external script""" @@ -521,7 +528,7 @@ class NodeSetupCLI: self.run_tunnel_manager_setup() if self.check_wg_enabled(): self.setup_test_wg_ip_tables() - self.setup_test_wg_ip_tables() + self.quic_bridge_deploy() From dea8a287e6d2ca000447957405bb2e947890b848 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Tue, 11 Nov 2025 19:42:03 +0100 Subject: [PATCH 004/180] add arguments for env vars --- scripts/nym-node-setup/nym-node-cli.py | 219 ++++++++++++++++++------- 1 file changed, 157 insertions(+), 62 deletions(-) diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index 6b001df8d2..b7c59dfc16 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -22,18 +22,28 @@ class NodeSetupCLI: def __init__(self, args): self.branch = args.dev self.welcome_message = self.print_welcome_message() - self.mode = self.prompt_mode() + self.mode = self._get_or_prompt_mode(args) self.prereqs_install_sh = self.fetch_script("nym-node-prereqs-install.sh") - self.env_vars_install_sh = self.fetch_script("setup-env-vars.sh") + #self.env_vars_install_sh = self.fetch_script("setup-env-vars.sh") self.node_install_sh = self.fetch_script("nym-node-install.sh") self.service_config_sh = self.fetch_script("setup-systemd-service-file.sh") self.start_node_systemd_service_sh = self.fetch_script("start-node-systemd-service.sh") - self.landing_page_html = self._check_gwx_mode() and self.fetch_script("landing-page.html") - self.nginx_proxy_wss_sh = self._check_gwx_mode() and self.fetch_script("nginx_proxy_wss_sh") - self.tunnel_manager_sh = self._check_gwx_mode() and self.fetch_script("network_tunnel_manager.sh") - self.wg_ip_tables_manager_sh = self._check_gwx_mode() and self.fetch_script("wireguard-exit-policy-manager.sh") - self.wg_ip_tables_test_sh = self._check_gwx_mode() and self.fetch_script("exit-policy-tests.sh") - self.wg_ip_tables_test_sh = self._check_gwx_mode() and self.fetch_script("quic_bridge_deployment.sh") + self.is_gwx = self.mode == "exit-gateway" + if self.is_gwx: + self.landing_page_html = self.fetch_script("landing-page.html") + self.nginx_proxy_wss_sh = self.fetch_script("nginx_proxy_wss_sh") + self.tunnel_manager_sh = self.fetch_script("network_tunnel_manager.sh") + self.wg_ip_tables_manager_sh = self.fetch_script("wireguard-exit-policy-manager.sh") + self.wg_ip_tables_test_sh = self.fetch_script("exit-policy-tests.sh") + self.quic_bridge_deployment_sh = self.fetch_script("quic_bridge_deployment.sh") + else: + self.landing_page_html = None + self.nginx_proxy_wss_sh = None + self.tunnel_manager_sh = None + self.wg_ip_tables_manager_sh = None + self.wg_ip_tables_test_sh = None + self.quic_bridge_deployment_sh = None + def print_welcome_message(self): """Welcome user, warns for needed pre-reqs and asks for confimation""" @@ -46,7 +56,7 @@ class NodeSetupCLI: self.print_character("=", 41) msg = \ "Before you begin, make sure that:\n"\ - "1. You run this setup on Debian based Linux (ie Ubuntu)\n"\ + "1. You run this setup on Debian based Linux (ie Ubuntu 22.04 LTS)\n"\ "2. You run this installation program from a root shell\n"\ "3. You meet minimal requirements: https://nym.com/docs/operators/nodes\n"\ "4. You accept Operators Terms & Conditions: https://nym.com/operators-validators-terms\n"\ @@ -60,43 +70,99 @@ class NodeSetupCLI: else: print("Without confirming the points above, we cannot continue.") exit(1) + + 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): "), + ("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: "), + ] - def prompt_mode(self): - """Ask user to insert node functionality and save it in python and bash envs""" + existing = self._read_env_file(env_file) + updated = {} + + 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 + + # autodetect PUBLIC_IP if not already set + if not os.environ.get("PUBLIC_IP"): + try: + ip = subprocess.run(["curl", "-fsS4", "https://ifconfig.me"], + capture_output=True, text=True, timeout=5) + if ip.returncode == 0 and ip.stdout.strip(): + updated["PUBLIC_IP"] = ip.stdout.strip() + os.environ["PUBLIC_IP"] = ip.stdout.strip() + except Exception: + pass + + # write all collected variables to env.sh in one go + self._upsert_env_vars(updated, env_file) + + print(f"[OK] Updated env.sh with {len(updated)} entries.") + + + + + def _upsert_env_vars(self, updates: dict, env_file: Path = Path("env.sh")): + existing = self._read_env_file(env_file) + existing.update(updates) + with env_file.open("w") as f: + for k, v in existing.items(): + f.write(f'export {k}="{v}"\n') + os.environ.update(updates) + + def _read_env_file(self, env_file: Path) -> dict: + env = {} + if env_file.exists(): + for line in env_file.read_text().splitlines(): + if line.startswith("export ") and "=" in line: + k, v = line.replace("export ", "", 1).split("=", 1) + env[k.strip()] = v.strip().strip('"') + return env + + def _get_or_prompt_mode(self, args): + """Resolve MODE from --mode, env.sh, os.environ, or prompt; persist to env.sh.""" + + env_file = Path("env.sh") + + # CLI arg + mode = getattr(args, "mode", None) + if mode: + mode = mode.strip().lower() + self._upsert_env_var("MODE", mode) + print(f"Mode set to '{mode}' from CLI argument.") + return mode + + # env.sh (replaces manual read) + existing = self._read_env_file(env_file) + mode = existing.get("MODE") + if mode: + os.environ["MODE"] = mode + return mode + + # process env + if os.environ.get("MODE"): + return os.environ["MODE"] + + # prompt mode = input( - "\nEnter the mode you want to run nym-node in: " - "\n1. mixnode " - "\n2. entry-gateway " - "\n3. exit-gateway (works as entry-gateway as well) " - "\nPress 1, 2 or 3 and enter:\n" - ).strip() - - if mode in ("1", "mixnode"): - mode = "mixnode" - elif mode in ("2", "entry-gateway"): - mode = "entry-gateway" - elif mode in ("3", "exit-gateway"): - mode = "exit-gateway" - else: - print("Only numbers 1, 2 or 3 are accepted.") + "\nEnter node mode (mixnode / entry-gateway / exit-gateway): " + ).strip().lower() + if mode not in ("mixnode", "entry-gateway", "exit-gateway"): + print("Invalid mode. Must be one of: mixnode, entry-gateway, exit-gateway.") raise SystemExit(1) - # save mode for this Python instance - self.mode = mode - os.environ["MODE"] = mode - - # persist to env.sh so other scripts can source it - env_file = Path("env.sh") - with env_file.open("a") as f: - f.write(f'export MODE="{mode}"\n') - - # source env.sh so future bash subprocesses see it immediately - subprocess.run("source ./env.sh", shell=True, executable="/bin/bash") - + self._upsert_env_var("MODE", mode) print(f"Mode set to '{mode}' — stored in env.sh and sourced for immediate use.") return mode - def fetch_script(self, script_name): """Fetches needed scripts according to a defined mode""" # print header only the first time @@ -120,7 +186,7 @@ class NodeSetupCLI: github_raw_nymtech_nym_scripts_url = f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/{self.branch}/scripts/" scripts_urls = { "nym-node-prereqs-install.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/nym-node-prereqs-install.sh", - "setup-env-vars.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-env-vars.sh", + #"setup-env-vars.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-env-vars.sh", "nym-node-install.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/nym-node-install.sh", "setup-systemd-service-file.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-systemd-service-file.sh", "start-node-systemd-service.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/start-node-systemd-service.sh", @@ -208,27 +274,30 @@ class NodeSetupCLI: else: return False - def check_wg_enabled(self): - """Checks if Wireguard is enabled and if not, prompts user if they want to enable it, stores it to env.sh""" + def check_wg_enabled(self, args=None): + """Determine if WireGuard is enabled; precedence: CLI > env > env.sh > prompt. Persist normalized value.""" + env_file = os.path.join(os.getcwd(), "env.sh") - env_file = os.path.abspath(os.path.join(os.getcwd(), "env.sh")) + def norm(v): + return "true" if str(v).strip().lower() in ("true") else "false" - def norm(v): # -> "true" or "false" - return "true" if str(v).strip().lower() in ("1", "true", "yes", "y") else "false" + val = None - # precedence: process env → env.sh → prompt - val = os.environ.get("WIREGUARD") + # CLI argument + if args and getattr(args, "wireguard", None) is not None: + val = norm(getattr(args, "wireguard")) + print(f"[INFO] WireGuard mode provided via CLI: {val}") - if val is None and os.path.isfile(env_file): - try: - with open(env_file, "r", encoding="utf-8") as f: - m = re.search(r'^\s*export\s+WIREGUARD\s*=\s*"?([^"\n]+)"?', f.read(), re.M) - if m: - val = m.group(1) - except Exception: - pass + # Environment variable + val = val or os.environ.get("WIREGUARD") + # env.sh file + if val is None: + envs = self._read_env_file(Path(env_file)) + val = envs.get("WIREGUARD") + + # Prompt if val is None: ans = input( "\nWireGuard is not configured.\n" @@ -240,25 +309,24 @@ class NodeSetupCLI: val = norm(val) os.environ["WIREGUARD"] = val - # persist to env.sh (replace or append) + # Persist to env.sh try: text = "" if os.path.isfile(env_file): - with open(env_file, "r", encoding="utf-8") as f: + with open(env_file, encoding="utf-8") as f: text = f.read() if re.search(r'^\s*export\s+WIREGUARD\s*=.*$', text, re.M): text = re.sub(r'^\s*export\s+WIREGUARD\s*=.*$', f'export WIREGUARD="{val}"', text, flags=re.M) else: - if text and not text.endswith("\n"): - text += "\n" - text += f'export WIREGUARD="{val}"\n' + text = (text.rstrip("\n") + "\n" if text else "") + f'export WIREGUARD="{val}"\n' with open(env_file, "w", encoding="utf-8") as f: f.write(text) print(f'WIREGUARD={val} saved to {env_file}') - except Exception as e: + except OSError as e: print(f"Warning: could not write {env_file}: {e}") - return (val == "true") + return val == "true" + def run_bash_command(self, command, args=None, *, env=None, cwd=None, check=True): """ @@ -517,6 +585,7 @@ class NodeSetupCLI: def run_node_installation(self,args): """Main function called by argparser command install running full node install flow""" + self.ensure_env_values(args) self.run_script(self.prereqs_install_sh) self.run_script(self.env_vars_install_sh) self.run_script(self.node_install_sh) @@ -565,6 +634,32 @@ class ArgParser: help="Starts nym-node installation setup CLI", aliases=["i", "I"], add_help=True ) + install_parser.add_argument( + "--mode", + choices=["mixnode", "entry-gateway", "exit-gateway"], + help="Node mode: 'mixnode', 'entry-gateway', or 'exit-gateway'", + ) + install_parser.add_argument( + "--wireguard", + choices=["true", "false"], + help="WireGuard functionality switch: true / false" + ) + install_parser.add_argument("--hostname", help="Node domain / hostname") + install_parser.add_argument("--location", help="Node location (country code or name)") + install_parser.add_argument("--email", help="Contact email for the node operator") + 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("--nym-node-binary", help="URL for nym-node binary (autodetected if omitted)") + + # generic fallback + install_parser.add_argument( + "--env", + action="append", + metavar="KEY=VALUE", + help="(Optional) Extra ENV VARS, e.g. --env CUSTOM_KEY=value", + ) + args = parser.parse_args() From cc74d218fcdb8817238f4fa5f5cde0a253bdc741 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Tue, 11 Nov 2025 21:56:15 +0100 Subject: [PATCH 005/180] rm redundant fn --- scripts/nym-node-setup/setup-env-vars.sh | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/scripts/nym-node-setup/setup-env-vars.sh b/scripts/nym-node-setup/setup-env-vars.sh index 3eafd93ba5..e119a61fd6 100644 --- a/scripts/nym-node-setup/setup-env-vars.sh +++ b/scripts/nym-node-setup/setup-env-vars.sh @@ -39,16 +39,6 @@ while true; do esac done -# try to get the latest binary URL (non-fatal if missing) -LATEST_BINARY=$( - curl -fsSL https://github.com/nymtech/nym/releases/latest \ - | grep -Eo 'href="/nymtech/nym/releases/download/[^"]+/nym-node"' \ - | head -n1 \ - | cut -d'"' -f2 -) -if [[ -z "${LATEST_BINARY:-}" ]]; then - echo "WARNING: Could not determine latest nym-node binary URL right now. The installer will resolve it later." -fi PUBLIC_IP=$(curl -fsS -4 https://ifconfig.me || true) PUBLIC_IP=${PUBLIC_IP:-""} From 9eca9efd823662b15d1769c76fa3425a5a8b5d1e Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Wed, 12 Nov 2025 13:45:33 +0100 Subject: [PATCH 006/180] fix direction and add test --- .../exit-policy-tests.sh | 68 ++++++++++++++++++- .../wireguard-exit-policy-manager.sh | 13 ++-- 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/scripts/wireguard-exit-policy/exit-policy-tests.sh b/scripts/wireguard-exit-policy/exit-policy-tests.sh index 5ae10b177c..e0f86e5969 100644 --- a/scripts/wireguard-exit-policy/exit-policy-tests.sh +++ b/scripts/wireguard-exit-policy/exit-policy-tests.sh @@ -47,7 +47,7 @@ test_port_range_rules() { "8087-8088:tcp:Simplify Media" "8232-8233:tcp:Zcash" "8332-8333:tcp:Bitcoin" - "18080-18081:tcp:Monero" + "18080-18081:tcp:Monero" ) local total_failures=0 @@ -146,6 +146,71 @@ test_critical_services() { return $failures } +# Test that the exit policy chain is correctly wired up to FORWARD chain +test_forward_chain_hook() { + echo -e "${YELLOW}Testing FORWARD Chain Hook...${NC}" + + local failures=0 + NETWORK_DEVICE=$(ip route show default | awk '/default/ {print $5}') + + if [[ -z "$NETWORK_DEVICE" ]]; then + echo -e "${RED}✗ Could not determine network device${NC}" + return 1 + fi + + # (incoming from nymwg, outgoing to network device) + if iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then + echo -e "${GREEN}✓ IPv4 FORWARD hook exists with correct direction: -i $WG_INTERFACE -o $NETWORK_DEVICE${NC}" + else + echo -e "${RED}✗ IPv4 FORWARD hook missing or has wrong direction${NC}" + echo -e "${YELLOW} Expected: -i $WG_INTERFACE -o $NETWORK_DEVICE -j $NYM_CHAIN${NC}" + + # Check if wrong direction exists + if iptables -C FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null; then + echo -e "${RED}✗ WRONG DIRECTION FOUND: -o $WG_INTERFACE (this matches return traffic, not client egress!)${NC}" + fi + + # Show what actually exists - specifically look for jump rules to NYM-EXIT + echo -e "${YELLOW} Current FORWARD rules that jump to $NYM_CHAIN:${NC}" + iptables -L FORWARD -n -v --line-numbers | grep -E "$NYM_CHAIN" || echo " (none found - exit policy chain is not hooked to FORWARD!)" + echo -e "${YELLOW} All FORWARD rules with $WG_INTERFACE (for reference):${NC}" + iptables -L FORWARD -n -v --line-numbers | grep -E "$WG_INTERFACE" | head -5 || echo " (none found)" + ((failures++)) + fi + + # Check IPv6 + if ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then + echo -e "${GREEN}✓ IPv6 FORWARD hook exists with correct direction: -i $WG_INTERFACE -o $NETWORK_DEVICE${NC}" + else + echo -e "${RED}✗ IPv6 FORWARD hook missing or has wrong direction${NC}" + echo -e "${YELLOW} Expected: -i $WG_INTERFACE -o $NETWORK_DEVICE -j $NYM_CHAIN${NC}" + + # Check if wrong direction exists + if ip6tables -C FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null; then + echo -e "${RED}✗ WRONG DIRECTION FOUND: -o $WG_INTERFACE (this matches return traffic, not client egress!)${NC}" + fi + + # Show what actually exists - specifically look for jump rules to NYM-EXIT + echo -e "${YELLOW} Current IPv6 FORWARD rules that jump to $NYM_CHAIN:${NC}" + ip6tables -L FORWARD -n -v --line-numbers | grep -E "$NYM_CHAIN" || echo " (none found - exit policy chain is not hooked to FORWARD!)" + echo -e "${YELLOW} All IPv6 FORWARD rules with $WG_INTERFACE (for reference):${NC}" + ip6tables -L FORWARD -n -v --line-numbers | grep -E "$WG_INTERFACE" | head -5 || echo " (none found)" + ((failures++)) + fi + + # Check rule position (should be before UFW rules) + local rule_num=$(iptables -L FORWARD -n --line-numbers | grep -E "$NYM_CHAIN.*$WG_INTERFACE.*$NETWORK_DEVICE" | awk '{print $1}' | head -1) + if [[ -n "$rule_num" ]]; then + if [[ $rule_num -le 5 ]]; then + echo -e "${GREEN}✓ Rule is early in FORWARD chain (position #$rule_num) - good for UFW compatibility${NC}" + else + echo -e "${YELLOW}⚠ Rule is later in FORWARD chain (position #$rule_num) - may conflict with UFW${NC}" + fi + fi + + return $failures +} + # Verify default reject rule exists test_default_reject_rule() { echo -e "${YELLOW}This test takes some time, do not quit the process${NC}" @@ -197,6 +262,7 @@ run_all_tests() { done local test_functions=( + "test_forward_chain_hook" "test_port_range_rules" "test_critical_services" ) diff --git a/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh b/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh index bce3d09711..b803cd65a3 100644 --- a/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh +++ b/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh @@ -11,7 +11,7 @@ # - Groups rules logically for easier management # - Integrates with existing Nym node configuration # -# Usage: ./nym-exit-policy.sh [command] +# Usage: ./wireguard-exit-policy-manager.sh [command] set -e @@ -117,15 +117,15 @@ create_nym_chain() { fi # Link it to the FORWARD chain if not already linked - if ! iptables -C FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null; then + if ! iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then echo -e "${YELLOW}Linking $NYM_CHAIN to FORWARD chain...${NC}" - iptables -A FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" + iptables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" fi # Link IPv6 chain - if ! ip6tables -C FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null; then + if ! ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then echo -e "${YELLOW}Linking $NYM_CHAIN to IPv6 FORWARD chain...${NC}" - ip6tables -A FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" + ip6tables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" fi } @@ -313,6 +313,7 @@ apply_port_allowlist() { ["IMAPOverTLS"]="993" ["POP3OverTLS"]="995" ["OpenVPN"]="1194" + ["WireGuardPeer"]="51820-51822" ["QTServerAdmin"]="1220" ["PKTKRB"]="1293" ["MSSQL"]="1433" @@ -418,7 +419,9 @@ clear_rules() { # Remove the chain from FORWARD if it exists iptables -D FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null || true + iptables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null || true ip6tables -D FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null || true + ip6tables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null || true # Delete the chains iptables -X "$NYM_CHAIN" 2>/dev/null || true From bdacc7200378e49eb04236aedf8e749cda4085b4 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Wed, 12 Nov 2025 15:22:26 +0100 Subject: [PATCH 007/180] rm redundant --- scripts/nym-node-setup/nym-node-cli.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index b7c59dfc16..06b8ef7d5f 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -24,7 +24,6 @@ class NodeSetupCLI: self.welcome_message = self.print_welcome_message() self.mode = self._get_or_prompt_mode(args) self.prereqs_install_sh = self.fetch_script("nym-node-prereqs-install.sh") - #self.env_vars_install_sh = self.fetch_script("setup-env-vars.sh") self.node_install_sh = self.fetch_script("nym-node-install.sh") self.service_config_sh = self.fetch_script("setup-systemd-service-file.sh") self.start_node_systemd_service_sh = self.fetch_script("start-node-systemd-service.sh") @@ -186,7 +185,6 @@ class NodeSetupCLI: github_raw_nymtech_nym_scripts_url = f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/{self.branch}/scripts/" scripts_urls = { "nym-node-prereqs-install.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/nym-node-prereqs-install.sh", - #"setup-env-vars.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-env-vars.sh", "nym-node-install.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/nym-node-install.sh", "setup-systemd-service-file.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-systemd-service-file.sh", "start-node-systemd-service.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/start-node-systemd-service.sh", From b56d9505e6c954446f8e7eeb5295bb60b8895e37 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Wed, 12 Nov 2025 15:31:08 +0100 Subject: [PATCH 008/180] address comments --- scripts/nym-node-setup/nym-node-cli.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index 06b8ef7d5f..6bbe7bd5e4 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -135,7 +135,7 @@ class NodeSetupCLI: mode = getattr(args, "mode", None) if mode: mode = mode.strip().lower() - self._upsert_env_var("MODE", mode) + self._upsert_env_vars("MODE", mode) print(f"Mode set to '{mode}' from CLI argument.") return mode @@ -158,7 +158,7 @@ class NodeSetupCLI: print("Invalid mode. Must be one of: mixnode, entry-gateway, exit-gateway.") raise SystemExit(1) - self._upsert_env_var("MODE", mode) + self._upsert_env_vars("MODE", mode) print(f"Mode set to '{mode}' — stored in env.sh and sourced for immediate use.") return mode @@ -278,7 +278,7 @@ class NodeSetupCLI: env_file = os.path.join(os.getcwd(), "env.sh") def norm(v): - return "true" if str(v).strip().lower() in ("true") else "false" + return "true" if str(v).strip().lower() == ("true") else "false" val = None @@ -391,8 +391,7 @@ class NodeSetupCLI: def quic_bridge_deploy(self): """Setup QUIC bridge and configuration using external script""" - self.run_script(self.quic_bridge_deployment_sh, args=["full_bridge_setup"], env=run_env) - return + self.run_script(self.quic_bridge_deployment_sh, args=["full_bridge_setup"]) def run_nym_node_as_service(self): """Starts /etc/systemd/system/nym-node.service based on prompt using external script""" From 0453345d65e60f828b1fa09af04a99b75f6142c8 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Wed, 12 Nov 2025 15:32:41 +0100 Subject: [PATCH 009/180] address comments --- scripts/nym-node-setup/nym-node-cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index 6bbe7bd5e4..c4ef41ca95 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -584,7 +584,6 @@ class NodeSetupCLI: """Main function called by argparser command install running full node install flow""" self.ensure_env_values(args) self.run_script(self.prereqs_install_sh) - self.run_script(self.env_vars_install_sh) self.run_script(self.node_install_sh) self.run_script(self.service_config_sh) self._check_gwx_mode() and self.run_script(self.nginx_proxy_wss_sh) From a8086675d9268207202f3281c9c036275aba7c49 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Wed, 12 Nov 2025 15:34:00 +0100 Subject: [PATCH 010/180] metadata port inside nymwg --- scripts/nym-node-setup/nym-node-prereqs-install.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/nym-node-setup/nym-node-prereqs-install.sh b/scripts/nym-node-setup/nym-node-prereqs-install.sh index 63f3264ff0..58b04ecc65 100644 --- a/scripts/nym-node-setup/nym-node-prereqs-install.sh +++ b/scripts/nym-node-setup/nym-node-prereqs-install.sh @@ -24,6 +24,7 @@ ufw allow 8080/tcp # Nym specific - nym-node-api ufw allow 9000/tcp # Nym Specific - clients port ufw allow 9001/tcp # Nym specific - wss port ufw allow 51822/udp # WireGuard +ufw allow in on nymwg to any port 51830 proto tcp # bandwidth queries/topup - inside the tunnel ufw allow 'Nginx Full' && \ ufw reload && \ ufw status From 73a34935aef7b6acc3ce8dc4ba80a14fae371076 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:29:52 +0100 Subject: [PATCH 011/180] trims --- scripts/nym-node-setup/nym-node-cli.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index c4ef41ca95..a56d46e84c 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -267,10 +267,7 @@ class NodeSetupCLI: def _check_gwx_mode(self): """Helper: Several fns run only for GWx - this fn checks this condition""" - if self.mode == "exit-gateway": - return True - else: - return False + return self.mode == "exit-gateway" def check_wg_enabled(self, args=None): """Determine if WireGuard is enabled; precedence: CLI > env > env.sh > prompt. Persist normalized value.""" @@ -609,7 +606,7 @@ class ArgParser: version=f"nym-node-cli {__version__}" ) parent.add_argument("-d", "--dev", metavar="BRANCH", - help="Define github branch", + help="Define github branch (default: develop)", type=str, default=argparse.SUPPRESS) parent.add_argument("-v", "--verbose", action="store_true", @@ -625,7 +622,7 @@ class ArgParser: subparsers = parser.add_subparsers(dest="command", help="subcommands") subparsers.required = True - p_install = subparsers.add_parser( + install_parser = subparsers.add_parser( "install", parents=[parent], help="Starts nym-node installation setup CLI", aliases=["i", "I"], add_help=True @@ -636,7 +633,7 @@ class ArgParser: help="Node mode: 'mixnode', 'entry-gateway', or 'exit-gateway'", ) install_parser.add_argument( - "--wireguard", + "--wireguard-enabled", choices=["true", "false"], help="WireGuard functionality switch: true / false" ) From 5a26fa262e0f39b9d9fd2e9e3822822bff970554 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:58:10 +0100 Subject: [PATCH 012/180] add uplink override arg --- scripts/nym-node-setup/nym-node-cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index a56d46e84c..2732f36756 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -644,6 +644,7 @@ class ArgParser: 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("--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)") # generic fallback install_parser.add_argument( From 66797efa805b0260901d02ccea0c651638c5e0db Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Wed, 12 Nov 2025 17:18:59 +0100 Subject: [PATCH 013/180] test new order of events.. --- .../wireguard-exit-policy-manager.sh | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh b/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh index b803cd65a3..5a31d8ed54 100644 --- a/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh +++ b/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh @@ -282,6 +282,16 @@ add_default_reject_rule() { apply_port_allowlist() { echo -e "${YELLOW}Applying allowed ports...${NC}" + # Insert DNS rules at the very beginning of NYM-EXIT chain, this ensures DNS queries are never blocked by REJECT rules + echo -e "${YELLOW}Ensuring DNS is at the beginning of $NYM_CHAIN...${NC}" + + iptables -I "$NYM_CHAIN" 1 -p udp --dport 53 -j ACCEPT + iptables -I "$NYM_CHAIN" 2 -p tcp --dport 53 -j ACCEPT + ip6tables -I "$NYM_CHAIN" 1 -p udp --dport 53 -j ACCEPT + ip6tables -I "$NYM_CHAIN" 2 -p tcp --dport 53 -j ACCEPT + + echo -e "${GREEN}✓ DNS rules inserted at beginning of $NYM_CHAIN${NC}" + # Dictionary of services and their ports declare -A PORT_MAPPINGS=( ["FTP"]="20-21" @@ -383,8 +393,6 @@ apply_port_allowlist() { add_port_rules ip6tables "$port" "udp" done - add_default_reject_rule - echo -e "${GREEN}Port allowlist applied successfully.${NC}" } @@ -643,8 +651,11 @@ main() { create_nym_chain setup_nat_rules configure_dns_and_icmp - apply_spamhaus_blocklist + # Apply allowlist first (so DNS and other allowed ports are not blocked) apply_port_allowlist + # Then apply blocklist (aka the REJECT rules) + apply_spamhaus_blocklist + add_default_reject_rule save_rules echo -e "${GREEN}Nym exit policy installed successfully.${NC}" ;; From 58083df0b092f96fce83a58b77917e676feed3da Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:22:43 +0100 Subject: [PATCH 014/180] fix QUIC helper script --- .../nym-node-setup/quic_bridge_deployment.sh | 50 +++++++++++++++++-- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/scripts/nym-node-setup/quic_bridge_deployment.sh b/scripts/nym-node-setup/quic_bridge_deployment.sh index f7dc60e0f9..1d40958578 100644 --- a/scripts/nym-node-setup/quic_bridge_deployment.sh +++ b/scripts/nym-node-setup/quic_bridge_deployment.sh @@ -32,6 +32,7 @@ log() { # simple redirection that keeps function scope intact add_log_redirection() { + exec 3>&1 4>&2 exec > >(tee -a "$LOG_FILE") 2>&1 } add_log_redirection @@ -47,7 +48,17 @@ NYM_ETC_BRIDGES="$NYM_ETC_DIR/bridges.toml" NYM_ETC_CLIENT_PARAMS_DEFAULT="$NYM_ETC_DIR/client_bridge_params.json" SERVICE_FILE="/etc/systemd/system/nym-bridge.service" -NET_DEV="$(ip route show default 2>/dev/null | awk '/default/ {print $5}' || true)" +NET_DEV="${UPLINK_DEV:-}" +if [[ -z "$NET_DEV" ]]; then + NET_DEV="$(ip -o route show default 2>/dev/null | awk '{print $5}' | head -n1)" + [[ -z "$NET_DEV" ]] && NET_DEV="$(ip -o route show default table all 2>/dev/null | awk '{print $5}' | head -n1)" +fi +if [[ -z "$NET_DEV" ]]; then + echo -e "${RED}Cannot determine uplink interface. Set UPLINK_DEV.${RESET}" | tee -a "$LOG_FILE" + exit 1 +fi +info "Using uplink device: $NET_DEV" + WG_IFACE="nymwg" # Root check @@ -65,6 +76,11 @@ err() { echo -e "${RED}$1${RESET}"; } info() { echo -e "${CYAN}$1${RESET}"; } press_enter() { read -r -p "$1"; } +# Disable pauses for noninteractive mode +if [[ "${NONINTERACTIVE:-0}" == "1" ]]; then + press_enter() { :; } +fi + # Helper: detect dpkg dependency failure for libc6>=2.34 deb_depends_libc_too_old() { local v @@ -377,6 +393,11 @@ User=root ExecStart=$BRIDGE_BIN --config $NYM_ETC_BRIDGES Restart=on-failure RestartSec=5 +LimitNOFILE=65535 +ProtectSystem=full +ProtectHome=yes +PrivateTmp=yes + [Install] WantedBy=multi-user.target @@ -391,12 +412,26 @@ EOF # IPTABLES & helpers apply_bridge_iptables_rules() { title "Applying iptables rules" - iptables -I INPUT -i "$WG_IFACE" -j ACCEPT || true - ip6tables -I INPUT -i "$WG_IFACE" -j ACCEPT || true - iptables -t nat -A POSTROUTING -o "$NET_DEV" -j MASQUERADE || true - ip6tables -t nat -A POSTROUTING -o "$NET_DEV" -j MASQUERADE || true + + # Ensure stateful rules exist + iptables -C FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ + iptables -I FORWARD 1 -m state --state RELATED,ESTABLISHED -j ACCEPT + ip6tables -C FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ + ip6tables -I FORWARD 1 -m state --state RELATED,ESTABLISHED -j ACCEPT + + # Allow WG interface input + iptables -C INPUT -i "$WG_IFACE" -j ACCEPT 2>/dev/null || iptables -I INPUT -i "$WG_IFACE" -j ACCEPT + ip6tables -C INPUT -i "$WG_IFACE" -j ACCEPT 2>/dev/null || ip6tables -I INPUT -i "$WG_IFACE" -j ACCEPT + + # NAT (idempotent) + iptables -t nat -C POSTROUTING -o "$NET_DEV" -j MASQUERADE 2>/dev/null || \ + iptables -t nat -I POSTROUTING 1 -o "$NET_DEV" -j MASQUERADE + ip6tables -t nat -C POSTROUTING -o "$NET_DEV" -j MASQUERADE 2>/dev/null || \ + ip6tables -t nat -I POSTROUTING 1 -o "$NET_DEV" -j MASQUERADE + iptables-save > /etc/iptables/rules.v4 ip6tables-save > /etc/iptables/rules.v6 + ok "iptables rules applied." } @@ -404,6 +439,9 @@ configure_dns_and_icmp() { title "Allow ICMP and DNS" iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT || true ip6tables -A INPUT -p ipv6-icmp -j ACCEPT || true + iptables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null || iptables -I INPUT -p udp --dport 53 -j ACCEPT + ip6tables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null || ip6tables -I INPUT -p udp --dport 53 -j ACCEPT + ok "ICMP and DNS rules applied." } @@ -429,6 +467,8 @@ full_bridge_setup() { echo "" echo "Step 2/6: Installing bridge binary..." install_bridge_binary + echo "[Bridge Install] $(date '+%F %T') $( $BRIDGE_BIN --version 2>/dev/null || echo 'nym-bridge (unknown)')" \ + >> /var/log/nym-bridge-version.log press_enter "Press Enter to continue..." echo "" From 781afd35226f3f3d83bf306530687e9537a0fb65 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:29:47 +0100 Subject: [PATCH 015/180] add arg --- scripts/nym-node-setup/nym-node-cli.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index 2732f36756..0fe9fa64e0 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -580,6 +580,9 @@ class NodeSetupCLI: def run_node_installation(self,args): """Main function called by argparser command install running full node install flow""" self.ensure_env_values(args) + # Pass uplink override to all helper scripts if provided + if getattr(args, "uplink_dev", None): + os.environ["UPLINK_DEV"] = args.uplink_dev self.run_script(self.prereqs_install_sh) self.run_script(self.node_install_sh) self.run_script(self.service_config_sh) From c503a5f0e8b0dce88fe0fb84e02b783542be4b73 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Thu, 13 Nov 2025 10:21:37 +0100 Subject: [PATCH 016/180] few more tweaks --- .../wireguard-exit-policy-manager.sh | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh b/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh index 5a31d8ed54..f3288b6204 100644 --- a/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh +++ b/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh @@ -290,7 +290,12 @@ apply_port_allowlist() { ip6tables -I "$NYM_CHAIN" 1 -p udp --dport 53 -j ACCEPT ip6tables -I "$NYM_CHAIN" 2 -p tcp --dport 53 -j ACCEPT - echo -e "${GREEN}✓ DNS rules inserted at beginning of $NYM_CHAIN${NC}" + # ICMP is needed because the probe tests DNS by pinging hostnames + iptables -I "$NYM_CHAIN" 3 -p icmp --icmp-type echo-request -j ACCEPT + iptables -I "$NYM_CHAIN" 4 -p icmp --icmp-type echo-reply -j ACCEPT + ip6tables -I "$NYM_CHAIN" 3 -p ipv6-icmp -j ACCEPT + + echo -e "${GREEN}✓ DNS and ICMP rules inserted at beginning of $NYM_CHAIN${NC}" # Dictionary of services and their ports declare -A PORT_MAPPINGS=( @@ -368,17 +373,18 @@ apply_port_allowlist() { ["Lightning"]="9735" ["NDMP"]="10000" ["OpenPGP"]="11371" - ["Monero"]="18080-18081" - ["MoneroRPC"]="18089" + ["Monero"]="18080-18081" + ["MoneroRPC"]="18089" ["GoogleVoice"]="19294" ["EnsimControlPanel"]="19638" - ["DarkFiTor"]="25551" + ["DarkFiTor"]="25551" ["Minecraft"]="25565" - ["DarkFi"]="26661" + ["DarkFi"]="26661" ["Steam"]="27000-27050" ["ElectrumSSL"]="50002" ["MOSH"]="60000-61000" ["Mumble"]="64738" + ["Metadata"]="51830" ) # Add TCP and UDP rules for each allowed port From 010b013094749db6fb3cbe04c5955b84ca9ffbf4 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 10:17:04 +0000 Subject: [PATCH 017/180] comment fix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/nym-node-cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index 0fe9fa64e0..590717ab2c 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -275,7 +275,7 @@ class NodeSetupCLI: env_file = os.path.join(os.getcwd(), "env.sh") def norm(v): - return "true" if str(v).strip().lower() == ("true") else "false" + return "true" if str(v).strip().lower() == "true" else "false" val = None From ef52f25564500fe449dea2954c523009585694f2 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:20:13 +0100 Subject: [PATCH 018/180] address comment --- scripts/nym-node-setup/nym-node-cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index 590717ab2c..6a3f84540a 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -158,7 +158,7 @@ class NodeSetupCLI: print("Invalid mode. Must be one of: mixnode, entry-gateway, exit-gateway.") raise SystemExit(1) - self._upsert_env_vars("MODE", mode) + self._upsert_env_vars({"MODE": mode}) print(f"Mode set to '{mode}' — stored in env.sh and sourced for immediate use.") return mode From 34a500d0a28c9013fe77b4a8175c3badf6224938 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:26:49 +0100 Subject: [PATCH 019/180] refactor completely --- scripts/network_tunnel_manager.sh | 290 -------- .../nym-node-setup/network-tunnel-manager.sh | 0 .../exit-policy-tests.sh | 306 -------- .../validate-exit-blocking-test.sh | 40 - .../wireguard-exit-policy-manager.sh | 699 ------------------ 5 files changed, 1335 deletions(-) delete mode 100644 scripts/network_tunnel_manager.sh create mode 100644 scripts/nym-node-setup/network-tunnel-manager.sh delete mode 100644 scripts/wireguard-exit-policy/exit-policy-tests.sh delete mode 100644 scripts/wireguard-exit-policy/validate-exit-blocking-test.sh delete mode 100644 scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh diff --git a/scripts/network_tunnel_manager.sh b/scripts/network_tunnel_manager.sh deleted file mode 100644 index 58eff34565..0000000000 --- a/scripts/network_tunnel_manager.sh +++ /dev/null @@ -1,290 +0,0 @@ -#!/bin/bash - -network_device=$(ip route show default | awk '/default/ {print $5}') -tunnel_interface="nymtun0" -wg_tunnel_interface="nymwg" - -if ! dpkg -s iptables-persistent >/dev/null 2>&1; then - sudo apt-get update - sudo apt-get install -y iptables-persistent -else - echo "iptables-persistent is already installed." -fi - -fetch_ipv6_address() { - local interface=$1 - ipv6_global_address=$(ip -6 addr show "$interface" scope global | grep inet6 | awk '{print $2}' | head -n 1) - - if [[ -z "$ipv6_global_address" ]]; then - echo "no globally routable IPv6 address found on $interface. Please configure IPv6 or check your network settings." - exit 1 - else - echo "using IPv6 address: $ipv6_global_address" - fi -} - -fetch_and_display_ipv6() { - ipv6_address=$(ip -6 addr show "$network_device" scope global | grep inet6 | awk '{print $2}') - if [[ -z "$ipv6_address" ]]; then - echo "no global IPv6 address found on $network_device." - else - echo "IPv6 address on $network_device: $ipv6_address" - fi -} - -remove_duplicate_rules() { - local interface=$1 - local script_name=$(basename "$0") - - if [[ -z "$interface" ]]; then - echo "error: no interface specified. please enter the interface (nymwg or nymtun0):" - read -r interface - fi - - if [[ "$interface" != "nymwg" && "$interface" != "nymtun0" ]]; then - echo "error: invalid interface '$interface'. allowed values are 'nymwg' or 'nymtun0'." >&2 - exit 1 - fi - - echo "removing duplicate rules for $interface..." - - iptables-save | grep "$interface" | while read -r line; do - sudo iptables -D ${line#-A } || echo "Failed to delete rule: $line" - done - - ip6tables-save | grep "$interface" | while read -r line; do - sudo ip6tables -D ${line#-A } || echo "Failed to delete rule: $line" - done - - echo "duplicates removed for $interface." - echo "!!-important-!! you need to now reapply the iptables rules for $interface." - if [ "$interface" == "nymwg" ]; then - echo "run: ./$script_name apply_iptables_rules_wg" - else - echo "run: ./$script_name apply_iptables_rules" - fi -} - -adjust_ip_forwarding() { - ipv6_forwarding_setting="net.ipv6.conf.all.forwarding=1" - ipv4_forwarding_setting="net.ipv4.ip_forward=1" - - # remove duplicate entries for these settings from the file - sudo sed -i "/^net.ipv6.conf.all.forwarding=/d" /etc/sysctl.conf - sudo sed -i "/^net.ipv4.ip_forward=/d" /etc/sysctl.conf - - echo "$ipv6_forwarding_setting" | sudo tee -a /etc/sysctl.conf - echo "$ipv4_forwarding_setting" | sudo tee -a /etc/sysctl.conf - - sudo sysctl -p /etc/sysctl.conf - -} - -apply_iptables_rules() { - local interface=$1 - echo "applying IPtables rules for $interface..." - sleep 2 - - sudo iptables -t nat -A POSTROUTING -o "$network_device" -j MASQUERADE - sudo iptables -A FORWARD -i "$interface" -o "$network_device" -j ACCEPT - sudo iptables -A FORWARD -i "$network_device" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT - - sudo ip6tables -t nat -A POSTROUTING -o "$network_device" -j MASQUERADE - sudo ip6tables -A FORWARD -i "$interface" -o "$network_device" -j ACCEPT - sudo ip6tables -A FORWARD -i "$network_device" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT - - sudo iptables-save | sudo tee /etc/iptables/rules.v4 - sudo ip6tables-save | sudo tee /etc/iptables/rules.v6 -} - -check_tunnel_iptables() { - local interface=$1 - echo "inspecting IPtables rules for $interface..." - echo "---------------------------------------" - echo "IPv4 rules:" - iptables -L FORWARD -v -n | awk -v dev="$interface" '/^Chain FORWARD/ || $0 ~ dev || $0 ~ "ufw-reject-forward"' - echo "---------------------------------------" - echo "IPv6 rules:" - ip6tables -L FORWARD -v -n | awk -v dev="$interface" '/^Chain FORWARD/ || $0 ~ dev || $0 ~ "ufw6-reject-forward"' -} - -check_ipv6_ipv4_forwarding() { - result_ipv4=$(cat /proc/sys/net/ipv4/ip_forward) - result_ipv6=$(cat /proc/sys/net/ipv6/conf/all/forwarding) - echo "IPv4 forwarding is $([ "$result_ipv4" == "1" ] && echo "enabled" || echo "not enabled")." - echo "IPv6 forwarding is $([ "$result_ipv6" == "1" ] && echo "enabled" || echo "not enabled")." -} - -check_ip_routing() { - echo "IPv4 routing table:" - ip route - echo "---------------------------------------" - echo "IPv6 routing table:" - ip -6 route -} - -perform_pings() { - echo "performing IPv4 ping to google.com..." - ping -c 4 google.com - echo "---------------------------------------" - echo "performing IPv6 ping to google.com..." - ping6 -c 4 google.com -} - -joke_through_tunnel() { - local interface=$1 - local green="\033[0;32m" - local reset="\033[0m" - local red="\033[0;31m" - local yellow="\033[0;33m" - - sleep 1 - echo - echo -e "${yellow}checking tunnel connectivity and fetching a joke for $interface...${reset}" - echo -e "${yellow}if these test succeeds, it confirms your machine can reach the outside world via IPv4 and IPv6.${reset}" - echo -e "${yellow}however, probes and external clients may experience different connectivity to your nym-node.${reset}" - - ipv4_address=$(ip addr show "$interface" | awk '/inet / {print $2}' | cut -d'/' -f1) - ipv6_address=$(ip addr show "$interface" | awk '/inet6 / && $2 !~ /^fe80/ {print $2}' | cut -d'/' -f1) - - if [[ -z "$ipv4_address" && -z "$ipv6_address" ]]; then - echo -e "${red}no IP address found on $interface. unable to fetch a joke.${reset}" - echo -e "${red}please verify your tunnel configuration and ensure the interface is up.${reset}" - return 1 - fi - - if [[ -n "$ipv4_address" ]]; then - echo - echo -e "------------------------------------" - echo -e "detected IPv4 address: $ipv4_address" - echo -e "testing IPv4 connectivity..." - echo - - if ping -c 1 -I "$ipv4_address" google.com >/dev/null 2>&1; then - echo -e "${green}IPv4 connectivity is working. fetching a joke...${reset}" - joke=$(curl -s -H "Accept: application/json" --interface "$ipv4_address" https://icanhazdadjoke.com/ | jq -r .joke) - [[ -n "$joke" && "$joke" != "null" ]] && echo -e "${green}IPv4 joke: $joke${reset}" || echo -e "failed to fetch a joke via IPv4." - else - echo -e "${red}IPv4 connectivity is not working for $interface. verify your routing and NAT settings.${reset}" - fi - else - echo -e "${red}no IPv4 address found on $interface. unable to fetch a joke via IPv4.${reset}" - fi - - if [[ -n "$ipv6_address" ]]; then - echo - echo -e "------------------------------------" - echo -e "detected IPv6 address: $ipv6_address" - echo -e "testing IPv6 connectivity..." - echo - - if ping6 -c 1 -I "$ipv6_address" google.com >/dev/null 2>&1; then - echo -e "${green}IPv6 connectivity is working. fetching a joke...${reset}" - joke=$(curl -s -H "Accept: application/json" --interface "$ipv6_address" https://icanhazdadjoke.com/ | jq -r .joke) - [[ -n "$joke" && "$joke" != "null" ]] && echo -e "${green}IPv6 joke: $joke${reset}" || echo -e "${red}failed to fetch a joke via IPv6.${reset}" - else - echo -e "${red}IPv6 connectivity is not working for $interface. verify your routing and NAT settings.${reset}" - fi - else - echo -e "${red}no IPv6 address found on $interface. unable to fetch a joke via IPv6.${reset}" - fi - - echo -e "${green}joke fetching processes completed for $interface.${reset}" - echo -e "------------------------------------" - - sleep 3 - echo - echo - echo -e "${yellow}### connectivity testing recommendations ###${reset}" - echo -e "${yellow}- use the following command to test WebSocket connectivity from an external client:${reset}" - echo -e "${yellow} wscat -c wss://:9001 ${reset}" - echo -e "${yellow}- test UDP connectivity on port 51822 (commonly used for nym wireguard) ${reset}" - echo -e "${yellow} from another machine, use tools like nc or socat to send UDP packets ${reset}" - echo -e "${yellow} echo 'test message' | nc -u 51822 ${reset}" - echo -e "${yellow}if connectivity issues persist, ensure port forwarding and firewall rules are correctly configured ${reset}" - echo -} - - -configure_dns_and_icmp_wg() { - echo "allowing icmp (ping)..." - sudo iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT - sudo iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT - - echo "allowing dns over udp (port 53)..." - sudo iptables -A INPUT -p udp --dport 53 -j ACCEPT - - echo "allowing dns over tcp (port 53)..." - sudo iptables -A INPUT -p tcp --dport 53 -j ACCEPT - - echo "saving iptables rules..." - sudo iptables-save >/etc/iptables/rules.v4 - - echo "dns and icmp configuration completed." -} - -case "$1" in -fetch_ipv6_address_nym_tun) - fetch_ipv6_address "$tunnel_interface" - ;; -fetch_and_display_ipv6) - fetch_and_display_ipv6 - ;; -apply_iptables_rules) - apply_iptables_rules "$tunnel_interface" - ;; -apply_iptables_rules_wg) - apply_iptables_rules "$wg_tunnel_interface" - ;; -check_nymtun_iptables) - check_tunnel_iptables "$tunnel_interface" - ;; -check_nym_wg_tun) - check_tunnel_iptables "$wg_tunnel_interface" - ;; -check_ipv6_ipv4_forwarding) - check_ipv6_ipv4_forwarding - ;; -check_ip_routing) - check_ip_routing - ;; -perform_pings) - perform_pings - ;; -joke_through_the_mixnet) - joke_through_tunnel "$tunnel_interface" - ;; -joke_through_wg_tunnel) - joke_through_tunnel "$wg_tunnel_interface" - ;; -configure_dns_and_icmp_wg) - configure_dns_and_icmp_wg - ;; -adjust_ip_forwarding) - adjust_ip_forwarding - ;; -remove_duplicate_rules) - remove_duplicate_rules "$2" - ;; -*) - echo "Usage: $0 [command]" - echo "Commands:" - echo " fetch_ipv6_address_nym_tun - Fetch IPv6 for nymtun0." - echo " fetch_and_display_ipv6 - Show IPv6 on default device." - echo " apply_iptables_rules - Apply IPtables rules for nymtun0." - echo " apply_iptables_rules_wg - Apply IPtables rules for nymwg." - echo " check_nymtun_iptables - Check IPtables for nymtun0." - echo " check_nym_wg_tun - Check IPtables for nymwg." - echo " check_ipv6_ipv4_forwarding - Check IPv4 and IPv6 forwarding." - echo " check_ip_routing - Display IP routing tables." - echo " perform_pings - Test IPv4 and IPv6 connectivity." - echo " joke_through_the_mixnet - Fetch a joke via nymtun0." - echo " joke_through_wg_tunnel - Fetch a joke via nymwg." - echo " configure_dns_and_icmp_wg - Allows icmp ping tests for probes alongside configuring dns" - echo " adjust_ip_forwarding - Enable IPV6 and IPV4 forwarding" - echo " remove_duplicate_rules - Remove duplicate iptables rules. Valid interfaces: nymwg, nymtun0" - exit 1 - ;; -esac - -echo "operation $1 completed successfully." diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh new file mode 100644 index 0000000000..e69de29bb2 diff --git a/scripts/wireguard-exit-policy/exit-policy-tests.sh b/scripts/wireguard-exit-policy/exit-policy-tests.sh deleted file mode 100644 index e0f86e5969..0000000000 --- a/scripts/wireguard-exit-policy/exit-policy-tests.sh +++ /dev/null @@ -1,306 +0,0 @@ -#!/bin/bash -# Nym Exit Policy Verification Unit Tests - -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[0;33m' -NC='\033[0m' - -NYM_CHAIN="NYM-EXIT" -WG_INTERFACE="nymwg" - -check_port_range_rules() { - local port_range="$1" - local protocol="${2:-tcp}" - local chain="${3:-$NYM_CHAIN}" - - # Extract start and end ports - local start_port=$(echo "$port_range" | cut -d'-' -f1) - local end_port=$(echo "$port_range" | cut -d'-' -f2) - - if iptables -t filter -C "$chain" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT 2>/dev/null; then - echo -e "${GREEN}✓ Rule exists: $chain $protocol port range $start_port:$end_port${NC}" - return 0 - else - echo -e "${RED}✗ Rule missing: $chain $protocol port range $start_port:$end_port${NC}" - - echo -e "${YELLOW}Dumping all rules in $chain:${NC}" - iptables -L "$chain" -n | grep "$protocol" - - return 1 - fi -} - -# Test port range rules -test_port_range_rules() { - echo -e "${YELLOW}Testing Port Range Rules...${NC}" - - # Select the essential port ranges for testing - local port_ranges=( - "20-21:tcp:FTP" - "80-81:tcp:HTTP" - "2082-2083:tcp:CPanel" - "5222-5223:tcp:XMPP" - "27000-27050:tcp:Steam (sampling)" - "989-990:tcp:FTP over TLS" - "5000-5005:tcp:RTP/VoIP" - "8087-8088:tcp:Simplify Media" - "8232-8233:tcp:Zcash" - "8332-8333:tcp:Bitcoin" - "18080-18081:tcp:Monero" - ) - - local total_failures=0 - - for range in "${port_ranges[@]}"; do - IFS=':' read -r port_range protocol service <<< "$range" - - # Extract start and end ports - local start_port=$(echo "$port_range" | cut -d'-' -f1) - local end_port=$(echo "$port_range" | cut -d'-' -f2) - - echo -e "${YELLOW}Testing $service $protocol port range $port_range${NC}" - - if iptables -t filter -C "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT 2>/dev/null; then - echo -e "${GREEN}✓ Rule exists: $NYM_CHAIN $protocol port range $start_port:$end_port${NC}" - else - echo -e "${RED}✗ Rule missing: $NYM_CHAIN $protocol port range $start_port:$end_port${NC}" - ((total_failures++)) - - echo -e "${YELLOW}Existing rules for protocol $protocol:${NC}" - iptables -L "$NYM_CHAIN" -n | grep "$protocol" - fi - done - - if [ $total_failures -eq 0 ]; then - return 0 - else - return 1 - fi -} - -test_critical_services() { - echo -e "${YELLOW}Testing Critical Service Rules...${NC}" - - local tcp_services=( - 22 # SSH - 53 # DNS - 443 # HTTPS - 853 # DNS over TLS - 1194 # OpenVPN - ) - - local udp_services=( - 53 # DNS - 123 # NTP - 1194 # OpenVPN - ) - - local failures=0 - - # Test TCP services - for port in "${tcp_services[@]}"; do - local rule_found=false - - # First check for exact match - if iptables -t filter -C "$NYM_CHAIN" -p tcp --dport "$port" -j ACCEPT 2>/dev/null; then - echo -e "${GREEN}✓ Rule exists: NYM-EXIT tcp port $port${NC}" - rule_found=true - else - # If not found as exact port, search for it in port ranges - # This checks if the port is covered by any range rule - if iptables-save | grep -E "^-A $NYM_CHAIN.*tcp.*dpts:" | grep -qP "dpts:(\d+:)?$port(:|\d+)" || \ - iptables-save | grep -E "^-A $NYM_CHAIN.*tcp.*dpts:" | grep -qP "dpts:$port:"; then - echo -e "${GREEN}✓ Rule exists: NYM-EXIT tcp port $port (covered by a range rule)${NC}" - rule_found=true - else - echo -e "${RED}✗ Rule missing: NYM-EXIT tcp port $port${NC}" - ((failures++)) - fi - fi - done - - # Test UDP services - similar approach - for port in "${udp_services[@]}"; do - local rule_found=false - - if iptables -t filter -C "$NYM_CHAIN" -p udp --dport "$port" -j ACCEPT 2>/dev/null; then - echo -e "${GREEN}✓ Rule exists: NYM-EXIT udp port $port${NC}" - rule_found=true - else - # If not found as exact port, search for it in port ranges - if iptables-save | grep -E "^-A $NYM_CHAIN.*udp.*dpts:" | grep -qP "dpts:(\d+:)?$port(:|\d+)" || \ - iptables-save | grep -E "^-A $NYM_CHAIN.*udp.*dpts:" | grep -qP "dpts:$port:"; then - echo -e "${GREEN}✓ Rule exists: NYM-EXIT udp port $port (covered by a range rule)${NC}" - rule_found=true - else - echo -e "${RED}✗ Rule missing: NYM-EXIT udp port $port${NC}" - ((failures++)) - fi - fi - done - - echo -e "${YELLOW}Relevant existing rules for HTTP (port 80):${NC}" - iptables-save | grep -E "$NYM_CHAIN.*tcp" | grep -E "(dpt|dpts):.*80" - - return $failures -} - -# Test that the exit policy chain is correctly wired up to FORWARD chain -test_forward_chain_hook() { - echo -e "${YELLOW}Testing FORWARD Chain Hook...${NC}" - - local failures=0 - NETWORK_DEVICE=$(ip route show default | awk '/default/ {print $5}') - - if [[ -z "$NETWORK_DEVICE" ]]; then - echo -e "${RED}✗ Could not determine network device${NC}" - return 1 - fi - - # (incoming from nymwg, outgoing to network device) - if iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then - echo -e "${GREEN}✓ IPv4 FORWARD hook exists with correct direction: -i $WG_INTERFACE -o $NETWORK_DEVICE${NC}" - else - echo -e "${RED}✗ IPv4 FORWARD hook missing or has wrong direction${NC}" - echo -e "${YELLOW} Expected: -i $WG_INTERFACE -o $NETWORK_DEVICE -j $NYM_CHAIN${NC}" - - # Check if wrong direction exists - if iptables -C FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null; then - echo -e "${RED}✗ WRONG DIRECTION FOUND: -o $WG_INTERFACE (this matches return traffic, not client egress!)${NC}" - fi - - # Show what actually exists - specifically look for jump rules to NYM-EXIT - echo -e "${YELLOW} Current FORWARD rules that jump to $NYM_CHAIN:${NC}" - iptables -L FORWARD -n -v --line-numbers | grep -E "$NYM_CHAIN" || echo " (none found - exit policy chain is not hooked to FORWARD!)" - echo -e "${YELLOW} All FORWARD rules with $WG_INTERFACE (for reference):${NC}" - iptables -L FORWARD -n -v --line-numbers | grep -E "$WG_INTERFACE" | head -5 || echo " (none found)" - ((failures++)) - fi - - # Check IPv6 - if ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then - echo -e "${GREEN}✓ IPv6 FORWARD hook exists with correct direction: -i $WG_INTERFACE -o $NETWORK_DEVICE${NC}" - else - echo -e "${RED}✗ IPv6 FORWARD hook missing or has wrong direction${NC}" - echo -e "${YELLOW} Expected: -i $WG_INTERFACE -o $NETWORK_DEVICE -j $NYM_CHAIN${NC}" - - # Check if wrong direction exists - if ip6tables -C FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null; then - echo -e "${RED}✗ WRONG DIRECTION FOUND: -o $WG_INTERFACE (this matches return traffic, not client egress!)${NC}" - fi - - # Show what actually exists - specifically look for jump rules to NYM-EXIT - echo -e "${YELLOW} Current IPv6 FORWARD rules that jump to $NYM_CHAIN:${NC}" - ip6tables -L FORWARD -n -v --line-numbers | grep -E "$NYM_CHAIN" || echo " (none found - exit policy chain is not hooked to FORWARD!)" - echo -e "${YELLOW} All IPv6 FORWARD rules with $WG_INTERFACE (for reference):${NC}" - ip6tables -L FORWARD -n -v --line-numbers | grep -E "$WG_INTERFACE" | head -5 || echo " (none found)" - ((failures++)) - fi - - # Check rule position (should be before UFW rules) - local rule_num=$(iptables -L FORWARD -n --line-numbers | grep -E "$NYM_CHAIN.*$WG_INTERFACE.*$NETWORK_DEVICE" | awk '{print $1}' | head -1) - if [[ -n "$rule_num" ]]; then - if [[ $rule_num -le 5 ]]; then - echo -e "${GREEN}✓ Rule is early in FORWARD chain (position #$rule_num) - good for UFW compatibility${NC}" - else - echo -e "${YELLOW}⚠ Rule is later in FORWARD chain (position #$rule_num) - may conflict with UFW${NC}" - fi - fi - - return $failures -} - -# Verify default reject rule exists -test_default_reject_rule() { - echo -e "${YELLOW}This test takes some time, do not quit the process${NC}" - echo - echo -e "${YELLOW}Testing Default Reject Rule...${NC}" - - # Try different patterns to detect the reject rule - if iptables -L "$NYM_CHAIN" | grep -q "REJECT.*all.*anywhere.*anywhere.*reject-with"; then - echo -e "${GREEN}✓ Default REJECT rule exists${NC}" - return 0 - elif iptables -L "$NYM_CHAIN" | grep -q "REJECT.*all -- .*everywhere.*everywhere"; then - echo -e "${GREEN}✓ Default REJECT rule exists${NC}" - return 0 - elif iptables -L "$NYM_CHAIN" | grep -q "REJECT.*all.*0.0.0.0/0.*0.0.0.0/0"; then - echo -e "${GREEN}✓ Default REJECT rule exists${NC}" - return 0 - elif iptables -n -L "$NYM_CHAIN" | grep -qE "REJECT.*all.*0.0.0.0/0.*0.0.0.0/0"; then - echo -e "${GREEN}✓ Default REJECT rule exists${NC}" - return 0 - elif iptables -L "$NYM_CHAIN" | tail -1 | grep -q "REJECT"; then - echo -e "${GREEN}✓ Default REJECT rule exists at the end of chain${NC}" - return 0 - else - echo -e "${RED}✗ Default REJECT rule missing${NC}" - # Display the last 3 rules in the chain for debugging - echo -e "${YELLOW}Last 3 rules in the chain:${NC}" - iptables -L "$NYM_CHAIN" | tail -3 - return 1 - fi -} - -run_all_tests() { - local total_failures=0 - local total_tests=0 - local skip_default_reject=false - - # Parse arguments - while [[ $# -gt 0 ]]; do - case "$1" in - --skip-default-reject) - skip_default_reject=true - shift - ;; - *) - echo -e "${RED}Unknown argument: $1${NC}" - exit 1 - ;; - esac - done - - local test_functions=( - "test_forward_chain_hook" - "test_port_range_rules" - "test_critical_services" - ) - - if [ "$skip_default_reject" = false ]; then - test_functions+=("test_default_reject_rule") - fi - - echo -e "${YELLOW}Running Nym Exit Policy Verification Tests...${NC}" - - for test_func in "${test_functions[@]}"; do - ((total_tests++)) - $test_func - if [ $? -ne 0 ]; then - ((total_failures++)) - echo -e "${RED}Test $test_func FAILED${NC}" - else - echo -e "${GREEN}Test $test_func PASSED${NC}" - fi - done - - echo -e "\n${YELLOW}Test Summary:${NC}" - echo -e "Total Tests: $total_tests" - echo -e "Failures: $total_failures" - - if [ $total_failures -eq 0 ]; then - echo -e "${GREEN}All Tests Passed Successfully!${NC}" - exit 0 - else - echo -e "${RED}Some Tests Failed. Please review the iptables configuration.${NC}" - exit 1 - fi -} - -if [[ $EUID -ne 0 ]]; then - echo -e "${RED}This script must be run as root${NC}" - exit 1 -fi - -# Run the tests -run_all_tests "$@" diff --git a/scripts/wireguard-exit-policy/validate-exit-blocking-test.sh b/scripts/wireguard-exit-policy/validate-exit-blocking-test.sh deleted file mode 100644 index e93d28a24f..0000000000 --- a/scripts/wireguard-exit-policy/validate-exit-blocking-test.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -validate_exit_policy() { - echo "=== Nym Exit Policy Blocking Validation ===" - - # Check iptables rules - echo "Checking iptables NYM-EXIT chain:" - sudo iptables -L NYM-EXIT -v -n - - # Test IP ranges and individual IPs - test_ips=( - "5.188.10.0/24" # Blocked network range - "31.132.36.50" # Specific blocked IP - "37.9.42.100" # Another blocked IP - ) - - for target in "${test_ips[@]}"; do - echo -e "\n\e[33mTesting blocking for $target\e[0m" - - # Multiple connection test methods - methods=( - "ping -c 4 -W 2" - "curl -m 5 http://$target" - "nc -z -w 5 $target 80" - "telnet $target 80" - ) - - for method in "${methods[@]}"; do - echo -n "Testing with $method: " - if sudo timeout 5 $method >/dev/null 2>&1; then - echo -e "\e[31mFAILED: Connection succeeded (Blocking ineffective)\e[0m" - else - echo -e "\e[32mBLOCKED\e[0m" - fi - done - done -} - -# Run the test -validate_exit_policy \ No newline at end of file diff --git a/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh b/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh deleted file mode 100644 index f3288b6204..0000000000 --- a/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh +++ /dev/null @@ -1,699 +0,0 @@ -#!/bin/bash -# -# Nym Wireguard Exit Policy Manager -# Version: 1.0.0 -# -# This script manages iptables rules for Nym Wireguard exit policies -# Features: -# - Implements the Nym exit policy from official documentation -# - Makes rules persistent across reboots -# - Provides commands to inspect and manage rules -# - Groups rules logically for easier management -# - Integrates with existing Nym node configuration -# -# Usage: ./wireguard-exit-policy-manager.sh [command] - -set -e - -NETWORK_DEVICE=$(ip route show default | awk '/default/ {print $5}') -WG_INTERFACE="nymwg" -NYM_CHAIN="NYM-EXIT" -POLICY_FILE="/etc/nym/exit-policy.txt" -EXIT_POLICY_LOCATION="https://nymtech.net/.wellknown/network-requester/exit-policy.txt" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' - -add_port_rules() { - local chain="$1" - local port="$2" - local protocol="${3:-tcp}" - - # Check if the port contains a range - if [[ "$port" == *"-"* ]]; then - # Port range handling - add as a single rule with a range - local start_port=$(echo "$port" | cut -d'-' -f1) - local end_port=$(echo "$port" | cut -d'-' -f2) - - if ! $chain -C "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT 2>/dev/null; then - $chain -A "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT - echo -e " ${GREEN}Added: $NYM_CHAIN $protocol port range $start_port:$end_port${NC}" - fi - else - # Single port handling - if ! $chain -C "$NYM_CHAIN" -p "$protocol" --dport "$port" -j ACCEPT 2>/dev/null; then - $chain -A "$NYM_CHAIN" -p "$protocol" --dport "$port" -j ACCEPT - echo -e " ${GREEN}Added: $NYM_CHAIN $protocol port $port${NC}" - fi - fi -} - -install_dependencies() { - if ! dpkg -s iptables-persistent >/dev/null 2>&1; then - echo -e "${YELLOW}Installing iptables-persistent...${NC}" - apt-get update - DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent - echo -e "${GREEN}iptables-persistent installed.${NC}" - else - echo -e "${GREEN}iptables-persistent is already installed.${NC}" - fi - - # Check for other required dependencies - for cmd in iptables ip6tables ip grep sed awk wget curl; do - if ! command -v "$cmd" &>/dev/null; then - echo -e "${YELLOW}Installing $cmd...${NC}" - apt-get install -y "$cmd" - fi - done -} - -configure_ip_forwarding() { - echo -e "${YELLOW}Configuring IP forwarding...${NC}" - - # Remove any existing forwarding settings to avoid duplicates - sed -i "/^net.ipv6.conf.all.forwarding=/d" /etc/sysctl.conf - sed -i "/^net.ipv4.ip_forward=/d" /etc/sysctl.conf - - # Add forwarding settings - echo "net.ipv6.conf.all.forwarding=1" | tee -a /etc/sysctl.conf - echo "net.ipv4.ip_forward=1" | tee -a /etc/sysctl.conf - - # Apply changes - sysctl -p /etc/sysctl.conf - - # Verify settings - ipv4_forwarding=$(cat /proc/sys/net/ipv4/ip_forward) - ipv6_forwarding=$(cat /proc/sys/net/ipv6/conf/all/forwarding) - - if [ "$ipv4_forwarding" == "1" ] && [ "$ipv6_forwarding" == "1" ]; then - echo -e "${GREEN}IP forwarding configured successfully.${NC}" - else - echo -e "${RED}Failed to configure IP forwarding.${NC}" - exit 1 - fi -} - -create_nym_chain() { - echo -e "${YELLOW}Creating Nym exit policy chain...${NC}" - - # Check if the chain already exists - if iptables -L "$NYM_CHAIN" &>/dev/null; then - echo -e "${YELLOW}Chain $NYM_CHAIN already exists. Flushing it...${NC}" - iptables -F "$NYM_CHAIN" - else - echo -e "${YELLOW}Creating chain $NYM_CHAIN...${NC}" - iptables -N "$NYM_CHAIN" - fi - - # Do the same for IPv6 - if ip6tables -L "$NYM_CHAIN" &>/dev/null; then - echo -e "${YELLOW}Chain $NYM_CHAIN already exists in ip6tables. Flushing it...${NC}" - ip6tables -F "$NYM_CHAIN" - else - echo -e "${YELLOW}Creating chain $NYM_CHAIN in ip6tables...${NC}" - ip6tables -N "$NYM_CHAIN" - fi - - # Link it to the FORWARD chain if not already linked - if ! iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then - echo -e "${YELLOW}Linking $NYM_CHAIN to FORWARD chain...${NC}" - iptables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" - fi - - # Link IPv6 chain - if ! ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then - echo -e "${YELLOW}Linking $NYM_CHAIN to IPv6 FORWARD chain...${NC}" - ip6tables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" - fi -} - -setup_nat_rules() { - echo -e "${YELLOW}Setting up NAT rules...${NC}" - - # Check if NAT rule for IPv4 exists - 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 - echo -e "${GREEN}Added IPv4 NAT rule.${NC}" - else - echo -e "${GREEN}IPv4 NAT rule already exists.${NC}" - fi - - # Check if NAT rule for IPv6 exists - 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 - echo -e "${GREEN}Added IPv6 NAT rule.${NC}" - else - echo -e "${GREEN}IPv6 NAT rule already exists.${NC}" - fi - - # Setup forwarding rules for Wireguard interface - if ! iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null; then - iptables -A FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT - echo -e "${GREEN}Added IPv4 forwarding rule (WG → Internet).${NC}" - fi - - if ! iptables -C FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then - iptables -A FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT - echo -e "${GREEN}Added IPv4 forwarding rule (Internet → WG for established connections).${NC}" - fi - - # IPv6 forwarding rules - if ! ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null; then - ip6tables -A FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT - echo -e "${GREEN}Added IPv6 forwarding rule (WG → Internet).${NC}" - fi - - if ! ip6tables -C FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then - ip6tables -A FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT - echo -e "${GREEN}Added IPv6 forwarding rule (Internet → WG for established connections).${NC}" - fi -} - -configure_dns_and_icmp() { - echo -e "${YELLOW}Configuring DNS and ICMP rules...${NC}" - - # ICMP rules for ping - if ! iptables -C INPUT -p icmp --icmp-type echo-request -j ACCEPT 2>/dev/null; then - iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT - echo -e "${GREEN}Added IPv4 ICMP rule (allow ping requests).${NC}" - fi - - if ! iptables -C OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT 2>/dev/null; then - iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT - echo -e "${GREEN}Added IPv4 ICMP rule (allow ping replies).${NC}" - fi - - # ICMPv6 rules for ping6 - if ! ip6tables -C INPUT -p ipv6-icmp -j ACCEPT 2>/dev/null; then - ip6tables -A INPUT -p ipv6-icmp -j ACCEPT - echo -e "${GREEN}Added IPv6 ICMP rule (allow ping6).${NC}" - fi - - # DNS rules - if ! iptables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null; then - iptables -A INPUT -p udp --dport 53 -j ACCEPT - echo -e "${GREEN}Added IPv4 DNS rule (UDP).${NC}" - fi - - if ! iptables -C INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null; then - iptables -A INPUT -p tcp --dport 53 -j ACCEPT - echo -e "${GREEN}Added IPv4 DNS rule (TCP).${NC}" - fi - - # IPv6 DNS rules - if ! ip6tables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null; then - ip6tables -A INPUT -p udp --dport 53 -j ACCEPT - echo -e "${GREEN}Added IPv6 DNS rule (UDP).${NC}" - fi - - if ! ip6tables -C INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null; then - ip6tables -A INPUT -p tcp --dport 53 -j ACCEPT - echo -e "${GREEN}Added IPv6 DNS rule (TCP).${NC}" - fi -} - -# Apply Spamhaus blocklist from the Nym exit policy -apply_spamhaus_blocklist() { - echo -e "${YELLOW}Applying Spamhaus blocklist...${NC}" - - # Create directory if not exists - mkdir -p "$(dirname "$POLICY_FILE")" - - # Try to download the policy file - echo -e "${YELLOW}Downloading exit policy from $EXIT_POLICY_LOCATION${NC}" - if ! wget -q "$EXIT_POLICY_LOCATION" -O "$POLICY_FILE" 2>/dev/null; then - echo -e "${RED}Failed to download exit policy. Using minimal blocklist.${NC}" - - # Create a minimal policy file with a few entries - cat >"$POLICY_FILE" </dev/null; then - iptables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT - fi - - # Apply IPv6 rules for IPv6 addresses - if [[ "$ip_range" == *":"* ]] && ! ip6tables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT 2>/dev/null; then - ip6tables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT - fi - fi - done - - echo -e "${GREEN}Blocklist applied successfully.${NC}" -} - -add_default_reject_rule() { - echo -e "${YELLOW}Adding default reject rule...${NC}" - - # First remove any existing plain reject rules (without specific destinations) - iptables -D "$NYM_CHAIN" -j REJECT 2>/dev/null || true - iptables -D "$NYM_CHAIN" -j REJECT --reject-with icmp-port-unreachable 2>/dev/null || true - ip6tables -D "$NYM_CHAIN" -j REJECT 2>/dev/null || true - ip6tables -D "$NYM_CHAIN" -j REJECT --reject-with icmp6-port-unreachable 2>/dev/null || true - - # Add the default catch-all reject rule (must be the last rule in the chain) - iptables -A "$NYM_CHAIN" -j REJECT --reject-with icmp-port-unreachable - ip6tables -A "$NYM_CHAIN" -j REJECT --reject-with icmp6-port-unreachable - - echo -e "${GREEN}Default reject rule added successfully.${NC}" -} - -apply_port_allowlist() { - echo -e "${YELLOW}Applying allowed ports...${NC}" - - # Insert DNS rules at the very beginning of NYM-EXIT chain, this ensures DNS queries are never blocked by REJECT rules - echo -e "${YELLOW}Ensuring DNS is at the beginning of $NYM_CHAIN...${NC}" - - iptables -I "$NYM_CHAIN" 1 -p udp --dport 53 -j ACCEPT - iptables -I "$NYM_CHAIN" 2 -p tcp --dport 53 -j ACCEPT - ip6tables -I "$NYM_CHAIN" 1 -p udp --dport 53 -j ACCEPT - ip6tables -I "$NYM_CHAIN" 2 -p tcp --dport 53 -j ACCEPT - - # ICMP is needed because the probe tests DNS by pinging hostnames - iptables -I "$NYM_CHAIN" 3 -p icmp --icmp-type echo-request -j ACCEPT - iptables -I "$NYM_CHAIN" 4 -p icmp --icmp-type echo-reply -j ACCEPT - ip6tables -I "$NYM_CHAIN" 3 -p ipv6-icmp -j ACCEPT - - echo -e "${GREEN}✓ DNS and ICMP rules inserted at beginning of $NYM_CHAIN${NC}" - - # Dictionary of services and their ports - declare -A PORT_MAPPINGS=( - ["FTP"]="20-21" - ["SSH"]="22" - ["WHOIS"]="43" - ["DNS"]="53" - ["Finger"]="79" - ["HTTP"]="80-81" - ["Kerberos"]="88" - ["POP3"]="110" - ["NTP"]="123" - ["IMAP"]="143" - ["IMAP3"]="220" - ["LDAP"]="389" - ["HTTPS"]="443" - ["SMBWindowsFileShare"]="445" - ["Kpasswd"]="464" - ["RTSP"]="554" - ["LDAPS"]="636" - ["SILC"]="706" - ["KerberosAdmin"]="749" - ["DNSOverTLS"]="853" - ["Rsync"]="873" - ["VMware"]="902-904" - ["RemoteHTTPS"]="981" - ["FTPOverTLS"]="989-990" - ["NetnewsAdmin"]="991" - ["TelnetOverTLS"]="992" - ["IMAPOverTLS"]="993" - ["POP3OverTLS"]="995" - ["OpenVPN"]="1194" - ["WireGuardPeer"]="51820-51822" - ["QTServerAdmin"]="1220" - ["PKTKRB"]="1293" - ["MSSQL"]="1433" - ["VLSILicenseManager"]="1500" - ["OracleDB"]="1521" - ["Sametime"]="1533" - ["GroupWise"]="1677" - ["PPTP"]="1723" - ["RTSPAlt"]="1755" - ["MSNP"]="1863" - ["NFS"]="2049" - ["CPanel"]="2082-2083" - ["GNUnet"]="2086-2087" - ["NBX"]="2095-2096" - ["Zephyr"]="2102-2104" - ["XboxLive"]="3074" - ["MySQL"]="3306" - ["SVN"]="3690" - ["RWHOIS"]="4321" - ["Virtuozzo"]="4643" - ["RTPVOIP"]="5000-5005" - ["MMCC"]="5050" - ["ICQ"]="5190" - ["XMPP"]="5222-5223" - ["AndroidMarket"]="5228" - ["PostgreSQL"]="5432" - ["MongoDBDefault"]="27017" - ["Electrum"]="8082" - ["SimplifyMedia"]="8087-8088" - ["Zcash"]="8232-8233" - ["Bitcoin"]="8332-8333" - ["HTTPSALT"]="8443" - ["TeamSpeak"]="8767" - ["MQTTS"]="8883" - ["HTTPProxy"]="8888" - ["TorORPort"]="9001" - ["TorDirPort"]="9030" - ["Tari"]="9053" - ["Gaming"]="9339" - ["Git"]="9418" - ["HTTPSALT2"]="9443" - ["Lightning"]="9735" - ["NDMP"]="10000" - ["OpenPGP"]="11371" - ["Monero"]="18080-18081" - ["MoneroRPC"]="18089" - ["GoogleVoice"]="19294" - ["EnsimControlPanel"]="19638" - ["DarkFiTor"]="25551" - ["Minecraft"]="25565" - ["DarkFi"]="26661" - ["Steam"]="27000-27050" - ["ElectrumSSL"]="50002" - ["MOSH"]="60000-61000" - ["Mumble"]="64738" - ["Metadata"]="51830" - ) - - # Add TCP and UDP rules for each allowed port - for service in "${!PORT_MAPPINGS[@]}"; do - port="${PORT_MAPPINGS[$service]}" - echo -e "${YELLOW}Adding rules for $service (Port: $port)${NC}" - - # Add both TCP and UDP rules for all services - add_port_rules iptables "$port" "tcp" - add_port_rules ip6tables "$port" "tcp" - add_port_rules iptables "$port" "udp" - add_port_rules ip6tables "$port" "udp" - done - - echo -e "${GREEN}Port allowlist applied successfully.${NC}" -} - -safe_iptables_rule_remove() { - local chain="$1" - local table="${2:-filter}" - local interface="$3" - - # Remove rule if it exists - if iptables -t "$table" -C "$chain" -o "$interface" -j "$NYM_CHAIN" 2>/dev/null; then - iptables -t "$table" -D "$chain" -o "$interface" -j "$NYM_CHAIN" - fi -} - -safe_ip6tables_rule_remove() { - local chain="$1" - local table="${2:-filter}" - local interface="$3" - - # Remove rule if it exists - if ip6tables -t "$table" -C "$chain" -o "$interface" -j "$NYM_CHAIN" 2>/dev/null; then - ip6tables -t "$table" -D "$chain" -o "$interface" -j "$NYM_CHAIN" - fi -} - -clear_rules() { - echo -e "${YELLOW}Clearing Nym exit policy rules...${NC}" - - # Flush all rules in the NYM-EXIT chain - iptables -F "$NYM_CHAIN" 2>/dev/null || true - ip6tables -F "$NYM_CHAIN" 2>/dev/null || true - - # Remove the chain from FORWARD if it exists - iptables -D FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null || true - iptables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null || true - ip6tables -D FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null || true - ip6tables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null || true - - # Delete the chains - iptables -X "$NYM_CHAIN" 2>/dev/null || true - ip6tables -X "$NYM_CHAIN" 2>/dev/null || true - - echo -e "${GREEN}Nym exit policy rules cleared successfully.${NC}" -} - -remove_duplicate_rules() { - local interface="$1" - - if [[ -z "$interface" ]]; then - echo -e "${RED}Error: No interface specified. Usage: $0 remove-duplicates ${NC}" >&2 - exit 1 - fi - - echo -e "${YELLOW}Detecting and removing duplicate rules for $interface...${NC}" - - # Verbose duplicate rule detection for IPv4 - echo -e "${YELLOW}Checking IPv4 duplicate rules:${NC}" - iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq -d && { - echo -e "${RED}Duplicate IPv4 rules found! Removing...${NC}" - # Remove duplicates by saving unique rules - iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq | while read -r rule; do - # Carefully remove duplicates - full_rule=$(echo "$rule" | sed 's/^-A/iptables -D/') - eval "$full_rule" 2>/dev/null - done - } || echo -e "${GREEN}No duplicate IPv4 rules found.${NC}" - - # Verbose duplicate rule detection for IPv6 - echo -e "${YELLOW}Checking IPv6 duplicate rules:${NC}" - ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq -d && { - echo -e "${RED}Duplicate IPv6 rules found! Removing...${NC}" - # Remove duplicates by saving unique rules - ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq | while read -r rule; do - # Carefully remove duplicates - full_rule=$(echo "$rule" | sed 's/^-A/ip6tables -D/') - eval "$full_rule" 2>/dev/null - done - } || echo -e "${GREEN}No duplicate IPv6 rules found.${NC}" - - # Additional verification - echo -e "\n${YELLOW}Rule verification:${NC}" - echo "IPv4 Rules:" - iptables -L FORWARD -v -n | grep "$interface" - echo "IPv6 Rules:" - ip6tables -L FORWARD -v -n | grep "$interface" - - echo -e "${GREEN}Duplicate rule removal process completed.${NC}" -} - -save_rules() { - echo -e "${YELLOW}Saving iptables rules to make them persistent...${NC}" - - if [ -d "/etc/iptables" ]; then - # For Debian/Ubuntu with iptables-persistent - iptables-save >/etc/iptables/rules.v4 - ip6tables-save >/etc/iptables/rules.v6 - echo -e "${GREEN}Rules saved to /etc/iptables/rules.v4 and /etc/iptables/rules.v6${NC}" - else - # Fallback method - iptables-save >/etc/iptables.rules - ip6tables-save >/etc/ip6tables.rules - echo -e "${GREEN}Rules saved to /etc/iptables.rules and /etc/ip6tables.rules${NC}" - - # Add loading script to rc.local if it doesn't exist - if [ ! -f "/etc/network/if-pre-up.d/iptables" ]; then - cat >/etc/network/if-pre-up.d/iptables </dev/null; then - echo -e "${RED}WARNING: Wireguard interface $WG_INTERFACE not found!${NC}" - return 1 - fi - - # Interface details - echo -e "\n${YELLOW}Interface Details:${NC}" - ip link show "$WG_INTERFACE" - - # IP Addresses - echo -e "\n${YELLOW}IP Addresses:${NC}" - ip -4 addr show dev "$WG_INTERFACE" - ip -6 addr show dev "$WG_INTERFACE" - - # Iptables Chain Status - echo -e "\n${YELLOW}Iptables Chains:${NC}" - { - echo "IPv4 Chain:" - iptables -L "$NYM_CHAIN" -n -v - echo -e "\nIPv6 Chain:" - ip6tables -L "$NYM_CHAIN" -n -v - } || echo "One or both chains not found" - - # Forwarding Status - echo -e "\n${YELLOW}IP Forwarding:${NC}" - echo "IPv4: $(cat /proc/sys/net/ipv4/ip_forward)" - echo "IPv6: $(cat /proc/sys/net/ipv6/conf/all/forwarding)" -} - -test_connectivity() { - echo -e "${YELLOW}Testing connectivity through $WG_INTERFACE...${NC}" - - # More comprehensive interface check - interface_info=$(ip link show "$WG_INTERFACE" 2>/dev/null) - - if [ -z "$interface_info" ]; then - echo -e "${RED}Interface $WG_INTERFACE not found!${NC}" - return 1 - fi - - # Check for multiple possible interface states - if ! echo "$interface_info" | grep -qE "state (UP|UNKNOWN|DORMANT)"; then - echo -e "${RED}Interface $WG_INTERFACE is not in an active state!${NC}" - echo "$interface_info" - return 1 - fi - - # Detailed interface information - echo -e "${GREEN}Interface Details:${NC}" - echo "$interface_info" - - # Get IP addresses with more robust method - ipv4_address=$(ip -4 addr show dev "$WG_INTERFACE" | grep -oP '(?<=inet\s)\d+\.\d+\.\d+\.\d+/\d+' | cut -d'/' -f1 | head -n1) - ipv6_address=$(ip -6 addr show dev "$WG_INTERFACE" scope global | grep -oP '(?<=inet6\s)[0-9a-f:]+/\d+' | cut -d'/' -f1 | head -n1) - - echo -e "${GREEN}IPv4 Address:${NC} ${ipv4_address:-Not found}" - echo -e "${GREEN}IPv6 Address:${NC} ${ipv6_address:-Not found}" - - # Connectivity tests - if [[ -n "$ipv4_address" ]]; then - echo -e "${YELLOW}Testing IPv4 connectivity from $ipv4_address...${NC}" - - # Ping test - if timeout 5 ping -c 3 -I "$ipv4_address" 8.8.8.8 >/dev/null 2>&1; then - echo -e "${GREEN}IPv4 connectivity to 8.8.8.8: Success${NC}" - else - echo -e "${RED}IPv4 connectivity to 8.8.8.8: Failed${NC}" - fi - - # DNS resolution test - if timeout 5 ping -c 3 -I "$ipv4_address" google.com >/dev/null 2>&1; then - echo -e "${GREEN}IPv4 DNS resolution: Success${NC}" - else - echo -e "${RED}IPv4 DNS resolution: Failed${NC}" - fi - - # HTTP(S) connectivity test - if command -v curl &>/dev/null; then - if timeout 5 curl -s --interface "$ipv4_address" -o /dev/null -w "%{http_code}" https://www.google.com | grep -q "200"; then - echo -e "${GREEN}IPv4 HTTPS connectivity: Success${NC}" - else - echo -e "${RED}IPv4 HTTPS connectivity: Failed${NC}" - fi - fi - else - echo -e "${RED}No IPv4 address configured on $WG_INTERFACE${NC}" - fi - - # Similar tests for IPv6 if available - if [[ -n "$ipv6_address" ]]; then - echo -e "${YELLOW}Testing IPv6 connectivity from $ipv6_address...${NC}" - - if timeout 5 ping6 -c 3 -I "$ipv6_address" 2001:4860:4860::8888 >/dev/null 2>&1; then - echo -e "${GREEN}IPv6 connectivity to Google DNS: Success${NC}" - else - echo -e "${RED}IPv6 connectivity to Google DNS: Failed${NC}" - fi - - if timeout 5 ping6 -c 3 -I "$ipv6_address" google.com >/dev/null 2>&1; then - echo -e "${GREEN}IPv6 DNS resolution: Success${NC}" - else - echo -e "${RED}IPv6 DNS resolution: Failed${NC}" - fi - - if command -v curl &>/dev/null; then - if timeout 5 curl -s --interface "$ipv6_address" -o /dev/null -w "%{http_code}" https://www.google.com | grep -q "200"; then - echo -e "${GREEN}IPv6 HTTPS connectivity: Success${NC}" - else - echo -e "${RED}IPv6 HTTPS connectivity: Failed${NC}" - fi - fi - else - echo -e "${YELLOW}No IPv6 address configured on $WG_INTERFACE${NC}" - fi - - echo -e "${GREEN}Connectivity tests completed.${NC}" -} - -main() { - # Check for root privileges - if [ "$(id -u)" -ne 0 ]; then - echo -e "${RED}This script must be run as root${NC}" >&2 - exit 1 - fi - - # Parse command-line arguments - case "$1" in - install) - install_dependencies - configure_ip_forwarding - create_nym_chain - setup_nat_rules - configure_dns_and_icmp - # Apply allowlist first (so DNS and other allowed ports are not blocked) - apply_port_allowlist - # Then apply blocklist (aka the REJECT rules) - apply_spamhaus_blocklist - add_default_reject_rule - save_rules - echo -e "${GREEN}Nym exit policy installed successfully.${NC}" - ;; - status) - show_status - ;; - test) - test_connectivity - ;; - clear) - clear_rules - echo -e "${GREEN}Nym exit policy rules cleared.${NC}" - ;; - remove-duplicates) - remove_duplicate_rules "$2" - ;; - help | --help | -h) - echo "Usage: $0 [command]" - echo "" - echo "Commands:" - echo " install Install and configure Nym exit policy" - echo " status Show current Nym exit policy status" - echo " test Test connectivity through Wireguard interface" - echo " clear Remove all Nym exit policy rules" - echo " remove-duplicates Remove duplicate iptables rules for an interface" - echo " help Show this help message" - ;; - *) - echo -e "${RED}Invalid command. Use '$0 help' for usage information.${NC}" >&2 - exit 1 - ;; - esac -} - -main "$@" From f402da8e6091b33750cde1e619d07cd29e042604 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:28:38 +0100 Subject: [PATCH 020/180] add new top manager tool --- .../nym-node-setup/network-tunnel-manager.sh | 965 ++++++++++++++++++ 1 file changed, 965 insertions(+) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index e69de29bb2..1c869a297a 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -0,0 +1,965 @@ +#!/bin/bash +# nym tunnel and wireguard exit policy manager +# run this script as root + +set -euo pipefail + +############################################################################### +# safety: must run as root +############################################################################### +if [ "$(id -u)" -ne 0 ]; then + echo "this script must be run as root" + exit 1 +fi + +############################################################################### +# basic config +############################################################################### + +TUNNEL_INTERFACE="${TUNNEL_INTERFACE:-nymtun0}" +WG_INTERFACE="${WG_INTERFACE:-nymwg}" + +# uplink device detection, can be overridden +NETWORK_DEVICE="${NETWORK_DEVICE:-}" +if [[ -z "$NETWORK_DEVICE" ]]; then + NETWORK_DEVICE="$(ip -o route show default 2>/dev/null | awk '{print $5}' | head -n1 || true)" +fi +if [[ -z "$NETWORK_DEVICE" ]]; then + NETWORK_DEVICE="$(ip -o route show default table all 2>/dev/null | awk '{print $5}' | head -n1 || true)" +fi +if [[ -z "$NETWORK_DEVICE" ]]; then + echo "cannot determine uplink interface. set NETWORK_DEVICE or UPLINK_DEV" + exit 1 +fi + +NYM_CHAIN="NYM-EXIT" +POLICY_FILE="/etc/nym/exit-policy.txt" +EXIT_POLICY_LOCATION="https://nymtech.net/.wellknown/network-requester/exit-policy.txt" + +# colors (no emojis) +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[0;33m' +NC='\033[0m' + +############################################################################### +# shared helpers +############################################################################### + +install_iptables_persistent() { + if ! dpkg -s iptables-persistent >/dev/null 2>&1; then + echo -e "${YELLOW}installing iptables-persistent${NC}" + apt-get update -y + DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent + else + echo -e "${GREEN}iptables-persistent is already installed${NC}" + fi +} + +adjust_ip_forwarding() { + echo -e "${YELLOW}configuring ip forwarding via /etc/sysctl.d/99-nym-forwarding.conf${NC}" + install -m 0644 /dev/null /etc/sysctl.d/99-nym-forwarding.conf + cat > /etc/sysctl.d/99-nym-forwarding.conf </dev/null || echo 0) + v6=$(cat /proc/sys/net/ipv6/conf/all/forwarding 2>/dev/null || echo 0) + + if [[ "$v4" == "1" && "$v6" == "1" ]]; then + echo -e "${GREEN}ipv4 and ipv6 forwarding enabled${NC}" + else + echo -e "${RED}warning: ip forwarding not fully enabled (ipv4=$v4 ipv6=$v6)${NC}" + fi +} + +save_iptables_rules() { + echo -e "${YELLOW}saving iptables rules to /etc/iptables${NC}" + mkdir -p /etc/iptables + iptables-save > /etc/iptables/rules.v4 + ip6tables-save > /etc/iptables/rules.v6 + echo -e "${GREEN}iptables rules saved${NC}" +} + +############################################################################### +# part 1: network tunnel manager (nymtun0 + nymwg base nat/forwarding) +############################################################################### + +fetch_ipv6_address() { + local interface=$1 + local ipv6_global_address + ipv6_global_address=$(ip -6 addr show "$interface" scope global | awk '/inet6/ {print $2}' | head -n 1) + + if [[ -z "$ipv6_global_address" ]]; then + echo "no globally routable ipv6 address found on $interface. please configure ipv6 or check your network settings" + exit 1 + else + echo "using ipv6 address: $ipv6_global_address" + fi +} + +fetch_and_display_ipv6() { + local ipv6_address + ipv6_address=$(ip -6 addr show "$NETWORK_DEVICE" scope global | awk '/inet6/ {print $2}') + if [[ -z "$ipv6_address" ]]; then + echo "no global ipv6 address found on $NETWORK_DEVICE" + else + echo "ipv6 address on $NETWORK_DEVICE: $ipv6_address" + fi +} + +# dedupe / clean-up rules for an interface in FORWARD and NYM-EXIT +# keeps a single copy of each rule +remove_duplicate_rules() { + local interface="$1" + + if [[ -z "$interface" ]]; then + echo "error: no interface specified. usage: $0 remove_duplicate_rules " + exit 1 + fi + + echo -e "${YELLOW}detecting and removing duplicate rules for $interface in FORWARD and ${NYM_CHAIN}${NC}" + + # ipv4 + local rules_v4 + rules_v4=$(iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" || true) + if [[ -n "$rules_v4" ]]; then + echo -e "${YELLOW}processing ipv4 rules${NC}" + echo "$rules_v4" | sort | uniq -d | while read -r dup; do + echo "removing duplicate ipv4 rule: $dup" + while iptables -t filter -C ${dup#-A } 2>/dev/null; do + iptables -t filter -D ${dup#-A } || break + done + done + else + echo -e "${GREEN}no ipv4 rules found for $interface to deduplicate${NC}" + fi + + # ipv6 + local rules_v6 + rules_v6=$(ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" || true) + if [[ -n "$rules_v6" ]]; then + echo -e "${YELLOW}processing ipv6 rules${NC}" + echo "$rules_v6" | sort | uniq -d | while read -r dup; do + echo "removing duplicate ipv6 rule: $dup" + while ip6tables -t filter -C ${dup#-A } 2>/dev/null; do + ip6tables -t filter -D ${dup#-A } || break + done + done + else + echo -e "${GREEN}no ipv6 rules found for $interface to deduplicate${NC}" + fi + + echo -e "${GREEN}duplicate rule scan completed for $interface${NC}" +} + +apply_iptables_rules() { + local interface=$1 + echo "applying iptables rules for $interface using uplink $NETWORK_DEVICE" + sleep 1 + + # ipv4 nat and forwarding + iptables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null || \ + iptables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE + + iptables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null || \ + iptables -A FORWARD -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT + + iptables -C FORWARD -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ + iptables -A FORWARD -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT + + # ipv6 nat and forwarding + ip6tables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null || \ + ip6tables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE + + ip6tables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null || \ + ip6tables -A FORWARD -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT + + ip6tables -C FORWARD -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ + ip6tables -A FORWARD -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT + + save_iptables_rules +} + +check_tunnel_iptables() { + local interface=$1 + echo "inspecting iptables rules for $interface" + echo "ipv4 forward chain:" + iptables -L FORWARD -v -n | awk -v dev="$interface" '/^Chain FORWARD/ || $0 ~ dev || $0 ~ "ufw-reject-forward"' + echo + echo "ipv6 forward chain:" + ip6tables -L FORWARD -v -n | awk -v dev="$interface" '/^Chain FORWARD/ || $0 ~ dev || $0 ~ "ufw6-reject-forward"' +} + +check_ipv6_ipv4_forwarding() { + local result_ipv4 result_ipv6 + result_ipv4=$(cat /proc/sys/net/ipv4/ip_forward) + result_ipv6=$(cat /proc/sys/net/ipv6/conf/all/forwarding) + echo "ipv4 forwarding is $([ "$result_ipv4" == "1" ] && echo enabled || echo not enabled)" + echo "ipv6 forwarding is $([ "$result_ipv6" == "1" ] && echo enabled || echo not enabled)" +} + +check_ip_routing() { + echo "ipv4 routing table:" + ip route + echo "---------------------------" + echo "ipv6 routing table:" + ip -6 route +} + +perform_pings() { + echo "performing ipv4 ping to google.com" + ping -c 4 google.com || echo "ipv4 ping failed" + echo "---------------------------" + echo "performing ipv6 ping to google.com" + ping6 -c 4 google.com || echo "ipv6 ping failed" +} + +joke_through_tunnel() { + local interface=$1 + local green="\033[0;32m" + local reset="\033[0m" + local red="\033[0;31m" + local yellow="\033[0;33m" + + sleep 1 + echo + echo -e "${yellow}checking tunnel connectivity and fetching a joke for $interface${reset}" + echo -e "${yellow}if this test succeeds, it confirms your machine can reach the outside world via ipv4 and ipv6${reset}" + echo -e "${yellow}probes and external clients may still see different connectivity to your nym node${reset}" + + local ipv4_address ipv6_address joke + ipv4_address=$(ip addr show "$interface" | awk '/inet / {print $2}' | cut -d'/' -f1) + ipv6_address=$(ip addr show "$interface" | awk '/inet6 / && $2 !~ /^fe80/ {print $2}' | cut -d'/' -f1) + + if [[ -z "$ipv4_address" && -z "$ipv6_address" ]]; then + echo -e "${red}no ip address found on $interface. unable to fetch a joke${reset}" + echo -e "${red}please verify your tunnel configuration and ensure the interface is up${reset}" + return 1 + fi + + if [[ -n "$ipv4_address" ]]; then + echo + echo "------------------------------------" + echo "detected ipv4 address: $ipv4_address" + echo "testing ipv4 connectivity" + echo + + if ping -c 1 -I "$ipv4_address" google.com >/dev/null 2>&1; then + echo -e "${green}ipv4 connectivity is working. fetching a joke${reset}" + joke=$(curl -s -H "Accept: application/json" --interface "$ipv4_address" https://icanhazdadjoke.com/ | jq -r .joke || true) + [[ -n "$joke" && "$joke" != "null" ]] && echo -e "${green}ipv4 joke: $joke${reset}" || echo "failed to fetch a joke via ipv4" + else + echo -e "${red}ipv4 connectivity is not working for $interface. verify your routing and nat settings${reset}" + fi + else + echo -e "${red}no ipv4 address found on $interface. unable to fetch a joke via ipv4${reset}" + fi + + if [[ -n "$ipv6_address" ]]; then + echo + echo "------------------------------------" + echo "detected ipv6 address: $ipv6_address" + echo "testing ipv6 connectivity" + echo + + if ping6 -c 1 -I "$ipv6_address" google.com >/dev/null 2>&1; then + echo -e "${green}ipv6 connectivity is working. fetching a joke${reset}" + joke=$(curl -s -H "Accept: application/json" --interface "$ipv6_address" https://icanhazdadjoke.com/ | jq -r .joke || true) + [[ -n "$joke" && "$joke" != "null" ]] && echo -e "${green}ipv6 joke: $joke${reset}" || echo -e "${red}failed to fetch a joke via ipv6${reset}" + else + echo -e "${red}ipv6 connectivity is not working for $interface. verify your routing and nat settings${reset}" + fi + else + echo -e "${red}no ipv6 address found on $interface. unable to fetch a joke via ipv6${reset}" + fi + + echo -e "${green}joke fetching processes completed for $interface${reset}" + echo "------------------------------------" + + sleep 3 + echo + echo + echo -e "${yellow}connectivity testing recommendations${reset}" + echo -e "${yellow}- from another machine use wscat to test websocket connectivity on 9001${reset}" + echo -e "${yellow}- test udp connectivity on port 51822 (wireguard)${reset}" + echo -e "${yellow}- example: echo 'test' | nc -u 51822${reset}" +} + +configure_dns_and_icmp_wg() { + echo "allowing ping (icmp) and dns on this host" + iptables -C INPUT -p icmp --icmp-type echo-request -j ACCEPT 2>/dev/null || \ + iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT + iptables -C OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT 2>/dev/null || \ + iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT + + iptables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null || \ + iptables -A INPUT -p udp --dport 53 -j ACCEPT + iptables -C INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null || \ + iptables -A INPUT -p tcp --dport 53 -j ACCEPT + + save_iptables_rules + echo "dns and icmp configuration completed" +} + +############################################################################### +# part 2: wireguard exit policy manager +############################################################################### + +add_port_rules() { + local cmd="$1" # iptables or ip6tables + local port="$2" + local protocol="${3:-tcp}" + + if [[ "$port" == *"-"* ]]; then + local start_port end_port + start_port=$(echo "$port" | cut -d'-' -f1) + end_port=$(echo "$port" | cut -d'-' -f2) + + if ! $cmd -C "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT 2>/dev/null; then + $cmd -A "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT + echo "added $cmd $NYM_CHAIN $protocol port range $start_port:$end_port" + fi + else + if ! $cmd -C "$NYM_CHAIN" -p "$protocol" --dport "$port" -j ACCEPT 2>/dev/null; then + $cmd -A "$NYM_CHAIN" -p "$protocol" --dport "$port" -j ACCEPT + echo "added $cmd $NYM_CHAIN $protocol port $port" + fi + fi +} + +exit_policy_install_deps() { + install_iptables_persistent + + for cmd in iptables ip6tables ip grep sed awk wget curl; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "installing dependency: $cmd" + apt-get install -y "$cmd" + fi + done +} + +create_nym_chain() { + echo "creating nym exit policy chain $NYM_CHAIN" + + if iptables -L "$NYM_CHAIN" >/dev/null 2>&1; then + iptables -F "$NYM_CHAIN" + else + iptables -N "$NYM_CHAIN" + fi + + if ip6tables -L "$NYM_CHAIN" >/dev/null 2>&1; then + ip6tables -F "$NYM_CHAIN" + else + ip6tables -N "$NYM_CHAIN" + fi + + if ! iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then + iptables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" + fi + + if ! ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then + ip6tables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" + fi +} + +setup_nat_rules() { + echo "setting up nat and forwarding rules for $WG_INTERFACE via $NETWORK_DEVICE" + + 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 ! 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 + + if ! iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null; then + iptables -A FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT + fi + if ! iptables -C FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then + iptables -A FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT + fi + + if ! ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null; then + ip6tables -A FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -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 -A FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT + fi +} + +configure_exit_dns_and_icmp() { + echo "ensuring dns and icmp are allowed inside nym exit chain" + + iptables -I "$NYM_CHAIN" 1 -p udp --dport 53 -j ACCEPT + iptables -I "$NYM_CHAIN" 2 -p tcp --dport 53 -j ACCEPT + ip6tables -I "$NYM_CHAIN" 1 -p udp --dport 53 -j ACCEPT + ip6tables -I "$NYM_CHAIN" 2 -p tcp --dport 53 -j ACCEPT + + iptables -I "$NYM_CHAIN" 3 -p icmp --icmp-type echo-request -j ACCEPT + iptables -I "$NYM_CHAIN" 4 -p icmp --icmp-type echo-reply -j ACCEPT + ip6tables -I "$NYM_CHAIN" 3 -p ipv6-icmp -j ACCEPT +} + +apply_port_allowlist() { + echo "applying allowed port list into ${NYM_CHAIN}" + + configure_exit_dns_and_icmp + + declare -A PORT_MAPPINGS=( + ["FTP"]="20-21" + ["SSH"]="22" + ["WHOIS"]="43" + ["DNS"]="53" + ["Finger"]="79" + ["HTTP"]="80-81" + ["Kerberos"]="88" + ["POP3"]="110" + ["NTP"]="123" + ["IMAP"]="143" + ["IMAP3"]="220" + ["LDAP"]="389" + ["HTTPS"]="443" + ["SMBWindowsFileShare"]="445" + ["Kpasswd"]="464" + ["RTSP"]="554" + ["LDAPS"]="636" + ["SILC"]="706" + ["KerberosAdmin"]="749" + ["DNSOverTLS"]="853" + ["Rsync"]="873" + ["VMware"]="902-904" + ["RemoteHTTPS"]="981" + ["FTPOverTLS"]="989-990" + ["NetnewsAdmin"]="991" + ["TelnetOverTLS"]="992" + ["IMAPOverTLS"]="993" + ["POP3OverTLS"]="995" + ["OpenVPN"]="1194" + ["WireGuardPeer"]="51820-51822" + ["QTServerAdmin"]="1220" + ["PKTKRB"]="1293" + ["MSSQL"]="1433" + ["VLSILicenseManager"]="1500" + ["OracleDB"]="1521" + ["Sametime"]="1533" + ["GroupWise"]="1677" + ["PPTP"]="1723" + ["RTSPAlt"]="1755" + ["MSNP"]="1863" + ["NFS"]="2049" + ["CPanel"]="2082-2083" + ["GNUnet"]="2086-2087" + ["NBX"]="2095-2096" + ["Zephyr"]="2102-2104" + ["XboxLive"]="3074" + ["MySQL"]="3306" + ["SVN"]="3690" + ["RWHOIS"]="4321" + ["Virtuozzo"]="4643" + ["RTPVOIP"]="5000-5005" + ["MMCC"]="5050" + ["ICQ"]="5190" + ["XMPP"]="5222-5223" + ["AndroidMarket"]="5228" + ["PostgreSQL"]="5432" + ["MongoDBDefault"]="27017" + ["Electrum"]="8082" + ["SimplifyMedia"]="8087-8088" + ["Zcash"]="8232-8233" + ["Bitcoin"]="8332-8333" + ["HTTPSALT"]="8443" + ["TeamSpeak"]="8767" + ["MQTTS"]="8883" + ["HTTPProxy"]="8888" + ["TorORPort"]="9001" + ["TorDirPort"]="9030" + ["Tari"]="9053" + ["Gaming"]="9339" + ["Git"]="9418" + ["HTTPSALT2"]="9443" + ["Lightning"]="9735" + ["NDMP"]="10000" + ["OpenPGP"]="11371" + ["Monero"]="18080-18081" + ["MoneroRPC"]="18089" + ["GoogleVoice"]="19294" + ["EnsimControlPanel"]="19638" + ["DarkFiTor"]="25551" + ["Minecraft"]="25565" + ["DarkFi"]="26661" + ["Steam"]="27000-27050" + ["ElectrumSSL"]="50002" + ["MOSH"]="60000-61000" + ["Mumble"]="64738" + ["Metadata"]="51830" + ) + + for service in "${!PORT_MAPPINGS[@]}"; do + local port="${PORT_MAPPINGS[$service]}" + echo "adding rules for $service (ports $port)" + add_port_rules iptables "$port" "tcp" + add_port_rules ip6tables "$port" "tcp" + add_port_rules iptables "$port" "udp" + add_port_rules ip6tables "$port" "udp" + done +} + +apply_spamhaus_blocklist() { + echo "applying spamhaus-like blocklist from $EXIT_POLICY_LOCATION" + + mkdir -p "$(dirname "$POLICY_FILE")" + + if ! wget -q "$EXIT_POLICY_LOCATION" -O "$POLICY_FILE" 2>/dev/null; then + echo "failed to download exit policy, using minimal blocklist" + cat >"$POLICY_FILE" </dev/null; then + iptables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT + fi + if [[ "$ip_range" == *":"* ]] && ! ip6tables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT 2>/dev/null; then + ip6tables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT + fi + fi + done +} + +add_default_reject_rule() { + echo "ensuring default reject rule at end of ${NYM_CHAIN}" + + iptables -D "$NYM_CHAIN" -j REJECT 2>/dev/null || true + iptables -D "$NYM_CHAIN" -j REJECT --reject-with icmp-port-unreachable 2>/dev/null || true + ip6tables -D "$NYM_CHAIN" -j REJECT 2>/dev/null || true + ip6tables -D "$NYM_CHAIN" -j REJECT --reject-with icmp6-port-unreachable 2>/dev/null || true + + iptables -A "$NYM_CHAIN" -j REJECT --reject-with icmp-port-unreachable + ip6tables -A "$NYM_CHAIN" -j REJECT --reject-with icmp6-port-unreachable +} + +clear_exit_policy_rules() { + echo "clearing nym exit policy rules" + + iptables -F "$NYM_CHAIN" 2>/dev/null || true + ip6tables -F "$NYM_CHAIN" 2>/dev/null || true + + iptables -D FORWARD -i "$WG_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 + + iptables -X "$NYM_CHAIN" 2>/dev/null || true + ip6tables -X "$NYM_CHAIN" 2>/dev/null || true +} + +show_exit_policy_status() { + echo "nym exit policy status" + echo "network device: $NETWORK_DEVICE" + echo "wireguard interface: $WG_INTERFACE" + echo + + if ! ip link show "$WG_INTERFACE" >/dev/null 2>&1; then + echo "warning: wireguard interface $WG_INTERFACE not found" + else + echo "interface details:" + ip link show "$WG_INTERFACE" + echo + echo "ipv4 addresses:" + ip -4 addr show dev "$WG_INTERFACE" + echo + echo "ipv6 addresses:" + ip -6 addr show dev "$WG_INTERFACE" + fi + + echo + echo "iptables chains for ${NYM_CHAIN}:" + iptables -L "$NYM_CHAIN" -n -v 2>/dev/null || echo "ipv4 chain not found" + echo + ip6tables -L "$NYM_CHAIN" -n -v 2>/dev/null || echo "ipv6 chain not found" + echo + echo "ip forwarding:" + echo "ipv4: $(cat /proc/sys/net/ipv4/ip_forward 2>/dev/null || echo 0)" + echo "ipv6: $(cat /proc/sys/net/ipv6/conf/all/forwarding 2>/dev/null || echo 0)" +} + +test_exit_policy_connectivity() { + echo "testing connectivity through $WG_INTERFACE" + + local iface_info + iface_info=$(ip link show "$WG_INTERFACE" 2>/dev/null || true) + if [[ -z "$iface_info" ]]; then + echo "interface $WG_INTERFACE not found" + return 1 + fi + + echo "interface:" + echo "$iface_info" + + local ipv4_address ipv6_address + ipv4_address=$(ip -4 addr show dev "$WG_INTERFACE" | awk '/inet / {print $2}' | cut -d'/' -f1 | head -n1) + ipv6_address=$(ip -6 addr show dev "$WG_INTERFACE" scope global | awk '/inet6/ {print $2}' | cut -d'/' -f1 | head -n1) + + echo "ipv4 address: ${ipv4_address:-none}" + echo "ipv6 address: ${ipv6_address:-none}" + + if [[ -n "$ipv4_address" ]]; then + echo "testing ipv4 ping to 8.8.8.8" + timeout 5 ping -c 3 -I "$ipv4_address" 8.8.8.8 >/dev/null 2>&1 && \ + echo "ipv4 ping ok" || echo "ipv4 ping failed" + + echo "testing ipv4 dns resolution" + timeout 5 ping -c 3 -I "$ipv4_address" google.com >/dev/null 2>&1 && \ + echo "ipv4 dns ok" || echo "ipv4 dns failed" + fi + + if [[ -n "$ipv6_address" ]]; then + echo "testing ipv6 ping to google dns" + timeout 5 ping6 -c 3 -I "$ipv6_address" 2001:4860:4860::8888 >/dev/null 2>&1 && \ + echo "ipv6 ping ok" || echo "ipv6 ping failed" + + echo "testing ipv6 dns resolution" + timeout 5 ping6 -c 3 -I "$ipv6_address" google.com >/dev/null 2>&1 && \ + echo "ipv6 dns ok" || echo "ipv6 dns failed" + fi + + echo "connectivity tests finished" +} + +############################################################################### +# part 3: exit policy verification tests +############################################################################### + +test_port_range_rules() { + echo "testing port range rules in ${NYM_CHAIN}" + + local port_ranges=( + "20-21:tcp:ftp" + "80-81:tcp:http" + "2082-2083:tcp:cpanel" + "5222-5223:tcp:xmpp" + "27000-27050:tcp:steam-sample" + "989-990:tcp:ftp-tls" + "5000-5005:tcp:rtp-voip" + "8087-8088:tcp:simplify-media" + "8232-8233:tcp:zcash" + "8332-8333:tcp:bitcoin" + "18080-18081:tcp:monero" + ) + + local failures=0 + for entry in "${port_ranges[@]}"; do + IFS=':' read -r range proto name <<<"$entry" + local start end + start=$(echo "$range" | cut -d'-' -f1) + end=$(echo "$range" | cut -d'-' -f2) + + if iptables -t filter -C "$NYM_CHAIN" -p "$proto" --dport "$start:$end" -j ACCEPT 2>/dev/null; then + echo "rule ok: $name $proto $range" + else + echo "missing rule: $name $proto $range" + ((failures++)) + fi + done + + return "$failures" +} + +test_critical_services() { + echo "testing critical service rules in ${NYM_CHAIN}" + + local tcp_ports=(22 53 443 853 1194) + local udp_ports=(53 123 1194) + local failures=0 + + for port in "${tcp_ports[@]}"; do + if iptables -t filter -C "$NYM_CHAIN" -p tcp --dport "$port" -j ACCEPT 2>/dev/null; then + echo "tcp port $port allowed" + else + if iptables-save | grep -E "^-A $NYM_CHAIN.*tcp.*dpts:" | grep -q "$port"; then + echo "tcp port $port allowed by range" + else + echo "tcp port $port missing" + ((failures++)) + fi + fi + done + + for port in "${udp_ports[@]}"; do + if iptables -t filter -C "$NYM_CHAIN" -p udp --dport "$port" -j ACCEPT 2>/dev/null; then + echo "udp port $port allowed" + else + if iptables-save | grep -E "^-A $NYM_CHAIN.*udp.*dpts:" | grep -q "$port"; then + echo "udp port $port allowed by range" + else + echo "udp port $port missing" + ((failures++)) + fi + fi + done + + return "$failures" +} + +test_forward_chain_hook() { + echo "testing forward chain hook direction for ${NYM_CHAIN}" + + local failures=0 + + if iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then + echo "ipv4 forward hook ok: -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN" + else + echo "ipv4 forward hook missing or wrong" + ((failures++)) + fi + + if ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then + echo "ipv6 forward hook ok: -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN" + else + echo "ipv6 forward hook missing or wrong" + ((failures++)) + fi + + return "$failures" +} + +test_default_reject_rule() { + echo "testing default reject rule at end of ${NYM_CHAIN}" + + if iptables -L "$NYM_CHAIN" | grep -q "REJECT"; then + echo "default reject present in ipv4 chain" + else + echo "default reject missing in ipv4 chain" + return 1 + fi +} + +exit_policy_run_tests() { + local skip_default=0 + while [[ $# -gt 0 ]]; do + case "$1" in + --skip-default-reject) skip_default=1; shift ;; + *) echo "unknown test option: $1"; return 1 ;; + esac + done + + local total=0 + local failed=0 + + test_forward_chain_hook || ((failed++)) + ((total++)) + + test_port_range_rules || ((failed++)) + ((total++)) + + test_critical_services || ((failed++)) + ((total++)) + + if [[ $skip_default -eq 0 ]]; then + test_default_reject_rule || ((failed++)) + ((total++)) + fi + + echo "tests run: $total, failures: $failed" + if [[ $failed -eq 0 ]]; then + echo "all exit policy tests passed" + else + echo "some exit policy tests failed" + fi + + return "$failed" +} + +############################################################################### +# part 4: high level workflows +############################################################################### + +full_tunnel_setup() { + # this mirrors your previous chain of calls but inside one script + echo "running full tunnel setup for ${TUNNEL_INTERFACE} and ${WG_INTERFACE}" + + check_tunnel_iptables "$TUNNEL_INTERFACE" + remove_duplicate_rules "$TUNNEL_INTERFACE" + remove_duplicate_rules "$WG_INTERFACE" + check_tunnel_iptables "$TUNNEL_INTERFACE" + + adjust_ip_forwarding + + apply_iptables_rules "$TUNNEL_INTERFACE" + check_tunnel_iptables "$TUNNEL_INTERFACE" + + apply_iptables_rules "$WG_INTERFACE" + + configure_dns_and_icmp_wg + adjust_ip_forwarding + check_ipv6_ipv4_forwarding + + joke_through_tunnel "$TUNNEL_INTERFACE" + joke_through_tunnel "$WG_INTERFACE" + + echo "full tunnel setup completed" +} + +exit_policy_install() { + echo "installing nym wireguard exit policy for ${WG_INTERFACE} via ${NETWORK_DEVICE}" + exit_policy_install_deps + adjust_ip_forwarding + create_nym_chain + setup_nat_rules + apply_port_allowlist + apply_spamhaus_blocklist + add_default_reject_rule + save_iptables_rules + echo "nym exit policy installed" +} + +complete_networking_configuration() { + echo "starting complete networking configuration: tunnels + exit policy" + + full_tunnel_setup + exit_policy_install + exit_policy_run_tests || echo "exit policy tests reported problems, please review output" + + echo "complete networking configuration finished" +} + +############################################################################### +# cli +############################################################################### + +cmd="${1:-help}" + +case "$cmd" in + # high level workflows + full_tunnel_setup) + full_tunnel_setup + ;; + exit_policy_install) + exit_policy_install + ;; + complete_networking_configuration) + complete_networking_configuration + ;; + + # tunnel manager cmds + fetch_ipv6_address_nym_tun) + fetch_ipv6_address "$TUNNEL_INTERFACE" + ;; + fetch_and_display_ipv6) + fetch_and_display_ipv6 + ;; + apply_iptables_rules) + apply_iptables_rules "$TUNNEL_INTERFACE" + ;; + apply_iptables_rules_wg) + apply_iptables_rules "$WG_INTERFACE" + ;; + check_nymtun_iptables) + check_tunnel_iptables "$TUNNEL_INTERFACE" + ;; + check_nym_wg_tun) + check_tunnel_iptables "$WG_INTERFACE" + ;; + check_ipv6_ipv4_forwarding) + check_ipv6_ipv4_forwarding + ;; + check_ip_routing) + check_ip_routing + ;; + perform_pings) + perform_pings + ;; + joke_through_the_mixnet) + joke_through_tunnel "$TUNNEL_INTERFACE" + ;; + joke_through_wg_tunnel) + joke_through_tunnel "$WG_INTERFACE" + ;; + configure_dns_and_icmp_wg) + configure_dns_and_icmp_wg + ;; + adjust_ip_forwarding) + adjust_ip_forwarding + ;; + remove_duplicate_rules) + remove_duplicate_rules "${2:-}" + ;; + + # exit policy manager cmds + exit_policy_status) + show_exit_policy_status + ;; + exit_policy_test_connectivity) + test_exit_policy_connectivity + ;; + exit_policy_clear) + clear_exit_policy_rules + ;; + exit_policy_tests) + shift + exit_policy_run_tests "$@" + ;; + + help|--help|-h) + cat < [args] + +high level workflows: + full_tunnel_setup run tunnel iptables and checks for nymtun0 and nymwg + exit_policy_install install and configure wireguard exit policy + complete_networking_configuration run tunnel setup, exit policy install and tests + +tunnel and nat helpers: + fetch_ipv6_address_nym_tun show global ipv6 address on ${TUNNEL_INTERFACE} + fetch_and_display_ipv6 show ipv6 on uplink ${NETWORK_DEVICE} + apply_iptables_rules apply nat/forward rules for ${TUNNEL_INTERFACE} + apply_iptables_rules_wg apply nat/forward rules for ${WG_INTERFACE} + check_nymtun_iptables inspect forward chain for ${TUNNEL_INTERFACE} + check_nym_wg_tun inspect forward chain for ${WG_INTERFACE} + check_ipv6_ipv4_forwarding show ipv4/ipv6 forwarding flags + check_ip_routing show ipv4 and ipv6 routes + perform_pings test ipv4 and ipv6 pings + joke_through_the_mixnet test via ${TUNNEL_INTERFACE} with joke + joke_through_wg_tunnel test via ${WG_INTERFACE} with joke + configure_dns_and_icmp_wg allow ping and dns on this host + adjust_ip_forwarding enable ipv4/ipv6 forwarding via sysctl.d + remove_duplicate_rules deduplicate rules for interface in FORWARD and ${NYM_CHAIN} + +exit policy manager: + exit_policy_install install exit policy (iptables rules and blocklist) + exit_policy_status show status of exit policy and forwarding + exit_policy_test_connectivity test connectivity via ${WG_INTERFACE} + exit_policy_clear remove ${NYM_CHAIN} chains and hooks + exit_policy_tests [--skip-default-reject] + run verification tests on exit policy + +environment overrides: + TUNNEL_INTERFACE default nymtun0 + WG_INTERFACE default nymwg + NETWORK_DEVICE uplink device, auto-detected if not set + +EOF + ;; + *) + echo "unknown command: $cmd" + echo "run with 'help' for usage" + exit 1 + ;; +esac + +echo "operation ${cmd} completed" From e815f08505762542b82b8b2eea8e3ce58168773b Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:38:15 +0000 Subject: [PATCH 021/180] address comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 1c869a297a..29ed4c802e 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -143,11 +143,17 @@ remove_duplicate_rules() { rules_v6=$(ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" || true) if [[ -n "$rules_v6" ]]; then echo -e "${YELLOW}processing ipv6 rules${NC}" - echo "$rules_v6" | sort | uniq -d | while read -r dup; do - echo "removing duplicate ipv6 rule: $dup" - while ip6tables -t filter -C ${dup#-A } 2>/dev/null; do - ip6tables -t filter -D ${dup#-A } || break - done + # For each unique rule, count occurrences and remove all but one + echo "$rules_v6" | sort | uniq | while read -r rule; do + count=$(echo "$rules_v6" | grep -Fx "$rule" | wc -l) + if [[ "$count" -gt 1 ]]; then + echo "removing $((count - 1)) duplicate(s) of ipv6 rule: $rule" + for ((i=1; i/dev/null; then + ip6tables -t filter -D ${rule#-A } || break + fi + done + fi done else echo -e "${GREEN}no ipv6 rules found for $interface to deduplicate${NC}" From 694135c81bfebcc9f1c743dd49af7d733647f2f2 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:38:45 +0000 Subject: [PATCH 022/180] address comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 29ed4c802e..b6babd8c65 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -531,7 +531,7 @@ EOF fi local total_rules - total_rules=$(grep -c "^ExitPolicy reject" "$POLICY_FILE" | grep -v "\*:\*" || true) + total_rules=$(grep "^ExitPolicy reject" "$POLICY_FILE" | grep -v "\*:\*" | wc -l || true) echo "processing $total_rules blocklist rules" grep "^ExitPolicy reject" "$POLICY_FILE" | grep -v "\*:\*" | while read -r line; do From 1f8144eb119a53b590d8ae4375e450fbd5d56614 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:47:02 +0100 Subject: [PATCH 023/180] add a safeguard --- .../nym-node-setup/network-tunnel-manager.sh | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index b6babd8c65..e50b4f8d5d 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -5,13 +5,31 @@ set -euo pipefail ############################################################################### -# safety: must run as root +# safety: must run as root, jq ############################################################################### if [ "$(id -u)" -ne 0 ]; then echo "this script must be run as root" exit 1 fi +echo "checking for jq..." + +if command -v jq >/dev/null 2>&1; then + echo "jq is already installed" + return 0 +fi + +echo "jq not found, installing..." +apt-get update -y +DEBIAN_FRONTEND=noninteractive apt-get install -y jq + +if command -v jq >/dev/null 2>&1; then + echo "jq installed successfully" +else + echo "failed to install jq" + return 1 +fi + ############################################################################### # basic config ############################################################################### @@ -966,6 +984,6 @@ EOF echo "run with 'help' for usage" exit 1 ;; + echo "operation ${cmd} completed" + ;; esac - -echo "operation ${cmd} completed" From c617bbb240d4fd16f9f7a32fd5d6e6d05be7a8ab Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:59:36 +0100 Subject: [PATCH 024/180] fix jq --- scripts/nym-node-setup/network-tunnel-manager.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index e50b4f8d5d..0083286aa2 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -16,7 +16,7 @@ echo "checking for jq..." if command -v jq >/dev/null 2>&1; then echo "jq is already installed" - return 0 + exit 0 fi echo "jq not found, installing..." @@ -27,7 +27,7 @@ if command -v jq >/dev/null 2>&1; then echo "jq installed successfully" else echo "failed to install jq" - return 1 + exit 1 fi ############################################################################### From aba6c9d4acc34ac824c77c97b6b65365705dcac2 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:04:17 +0100 Subject: [PATCH 025/180] fix exit message --- .../nym-node-setup/network-tunnel-manager.sh | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 0083286aa2..0a2a4c6f89 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -869,74 +869,94 @@ complete_networking_configuration() { cmd="${1:-help}" case "$cmd" in - # high level workflows full_tunnel_setup) full_tunnel_setup + status=$? ;; exit_policy_install) exit_policy_install + status=$? ;; complete_networking_configuration) complete_networking_configuration + status=$? ;; # tunnel manager cmds fetch_ipv6_address_nym_tun) fetch_ipv6_address "$TUNNEL_INTERFACE" + status=$? ;; fetch_and_display_ipv6) fetch_and_display_ipv6 + status=$? ;; apply_iptables_rules) apply_iptables_rules "$TUNNEL_INTERFACE" + status=$? ;; apply_iptables_rules_wg) apply_iptables_rules "$WG_INTERFACE" + status=$? ;; check_nymtun_iptables) check_tunnel_iptables "$TUNNEL_INTERFACE" + status=$? ;; check_nym_wg_tun) check_tunnel_iptables "$WG_INTERFACE" + status=$? ;; check_ipv6_ipv4_forwarding) check_ipv6_ipv4_forwarding + status=$? ;; check_ip_routing) check_ip_routing + status=$? ;; perform_pings) perform_pings + status=$? ;; joke_through_the_mixnet) joke_through_tunnel "$TUNNEL_INTERFACE" + status=$? ;; joke_through_wg_tunnel) joke_through_tunnel "$WG_INTERFACE" + status=$? ;; configure_dns_and_icmp_wg) configure_dns_and_icmp_wg + status=$? ;; adjust_ip_forwarding) adjust_ip_forwarding + status=$? ;; remove_duplicate_rules) remove_duplicate_rules "${2:-}" + status=$? ;; # exit policy manager cmds exit_policy_status) show_exit_policy_status + status=$? ;; exit_policy_test_connectivity) test_exit_policy_connectivity + status=$? ;; exit_policy_clear) clear_exit_policy_rules + status=$? ;; exit_policy_tests) shift exit_policy_run_tests "$@" + status=$? ;; help|--help|-h) @@ -978,12 +998,17 @@ environment overrides: NETWORK_DEVICE uplink device, auto-detected if not set EOF + status=0 ;; + *) echo "unknown command: $cmd" echo "run with 'help' for usage" exit 1 ;; - echo "operation ${cmd} completed" - ;; esac + +# Only print when operation succeeded +if [ "$status" -eq 0 ]; then + echo "operation ${cmd} completed" +fi \ No newline at end of file From 71301ee0cc0f2fcfa0c2a281065fb82f042c6f91 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:08:34 +0100 Subject: [PATCH 026/180] sync ipv4 w ipv6 --- .../nym-node-setup/network-tunnel-manager.sh | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 0a2a4c6f89..241fc1137d 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -131,6 +131,7 @@ fetch_and_display_ipv6() { # dedupe / clean-up rules for an interface in FORWARD and NYM-EXIT # keeps a single copy of each rule + remove_duplicate_rules() { local interface="$1" @@ -139,29 +140,35 @@ remove_duplicate_rules() { exit 1 fi - echo -e "${YELLOW}detecting and removing duplicate rules for $interface in FORWARD and ${NYM_CHAIN}${NC}" + echo "detecting and removing duplicate rules for $interface in FORWARD and ${NYM_CHAIN}" # ipv4 local rules_v4 rules_v4=$(iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" || true) + if [[ -n "$rules_v4" ]]; then - echo -e "${YELLOW}processing ipv4 rules${NC}" - echo "$rules_v4" | sort | uniq -d | while read -r dup; do - echo "removing duplicate ipv4 rule: $dup" - while iptables -t filter -C ${dup#-A } 2>/dev/null; do - iptables -t filter -D ${dup#-A } || break - done + echo "processing ipv4 rules" + echo "$rules_v4" | sort | uniq | while read -r rule; do + count=$(echo "$rules_v4" | grep -Fx "$rule" | wc -l) + if [[ "$count" -gt 1 ]]; then + echo "removing $((count - 1)) duplicate(s) of ipv4 rule: $rule" + for ((i=1; i/dev/null; then + iptables -t filter -D ${rule#-A } || break + fi + done + fi done else - echo -e "${GREEN}no ipv4 rules found for $interface to deduplicate${NC}" + echo "no ipv4 rules found for $interface to deduplicate" fi # ipv6 local rules_v6 rules_v6=$(ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" || true) + if [[ -n "$rules_v6" ]]; then - echo -e "${YELLOW}processing ipv6 rules${NC}" - # For each unique rule, count occurrences and remove all but one + echo "processing ipv6 rules" echo "$rules_v6" | sort | uniq | while read -r rule; do count=$(echo "$rules_v6" | grep -Fx "$rule" | wc -l) if [[ "$count" -gt 1 ]]; then @@ -174,12 +181,13 @@ remove_duplicate_rules() { fi done else - echo -e "${GREEN}no ipv6 rules found for $interface to deduplicate${NC}" + echo "no ipv6 rules found for $interface to deduplicate" fi - echo -e "${GREEN}duplicate rule scan completed for $interface${NC}" + echo "duplicate rule scan completed for $interface" } + apply_iptables_rules() { local interface=$1 echo "applying iptables rules for $interface using uplink $NETWORK_DEVICE" From e2fe3a60a692b6e95489a4309b212609c4bab279 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:01:23 +0000 Subject: [PATCH 027/180] address comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/wireguard-exit-policy/exit-policy-tests.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/wireguard-exit-policy/exit-policy-tests.sh b/scripts/wireguard-exit-policy/exit-policy-tests.sh index e0f86e5969..0734f19318 100644 --- a/scripts/wireguard-exit-policy/exit-policy-tests.sh +++ b/scripts/wireguard-exit-policy/exit-policy-tests.sh @@ -199,7 +199,9 @@ test_forward_chain_hook() { fi # Check rule position (should be before UFW rules) - local rule_num=$(iptables -L FORWARD -n --line-numbers | grep -E "$NYM_CHAIN.*$WG_INTERFACE.*$NETWORK_DEVICE" | awk '{print $1}' | head -1) + local rule_num=$(iptables -L FORWARD -n --line-numbers -v | awk -v chain="$NYM_CHAIN" -v in="$WG_INTERFACE" -v out="$NETWORK_DEVICE" ' + $0 ~ chain && $0 ~ in && $0 ~ out {print $1; exit} + ') if [[ -n "$rule_num" ]]; then if [[ $rule_num -le 5 ]]; then echo -e "${GREEN}✓ Rule is early in FORWARD chain (position #$rule_num) - good for UFW compatibility${NC}" From 70a119ac587f6cb3aea1d6004221519471e93baa Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:09:38 +0000 Subject: [PATCH 028/180] address comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 241fc1137d..021b8cd818 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -16,7 +16,7 @@ echo "checking for jq..." if command -v jq >/dev/null 2>&1; then echo "jq is already installed" - exit 0 + # continue script execution fi echo "jq not found, installing..." From d04b61a88bbd86bd9da45cccabeb7e9bc5c584e8 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:10:01 +0000 Subject: [PATCH 029/180] spacing Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 021b8cd818..729dec9767 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -517,7 +517,7 @@ apply_port_allowlist() { ["NDMP"]="10000" ["OpenPGP"]="11371" ["Monero"]="18080-18081" - ["MoneroRPC"]="18089" + ["MoneroRPC"]="18089" ["GoogleVoice"]="19294" ["EnsimControlPanel"]="19638" ["DarkFiTor"]="25551" From c6a0256b0386137fbcd2c73894d491a01c9c3b51 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:12:52 +0100 Subject: [PATCH 030/180] remove wrong stdout --- scripts/nym-node-setup/network-tunnel-manager.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 729dec9767..6d8626078e 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -142,6 +142,7 @@ remove_duplicate_rules() { echo "detecting and removing duplicate rules for $interface in FORWARD and ${NYM_CHAIN}" + # ipv4 local rules_v4 rules_v4=$(iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" || true) @@ -282,7 +283,7 @@ joke_through_tunnel() { if ping -c 1 -I "$ipv4_address" google.com >/dev/null 2>&1; then echo -e "${green}ipv4 connectivity is working. fetching a joke${reset}" - joke=$(curl -s -H "Accept: application/json" --interface "$ipv4_address" https://icanhazdadjoke.com/ | jq -r .joke || true) + joke=$(curl -s -H "Accept: application/json" --interface "$ipv4_address" https://icanhazdadjoke.com/ | jq -r .joke) [[ -n "$joke" && "$joke" != "null" ]] && echo -e "${green}ipv4 joke: $joke${reset}" || echo "failed to fetch a joke via ipv4" else echo -e "${red}ipv4 connectivity is not working for $interface. verify your routing and nat settings${reset}" @@ -300,7 +301,7 @@ joke_through_tunnel() { if ping6 -c 1 -I "$ipv6_address" google.com >/dev/null 2>&1; then echo -e "${green}ipv6 connectivity is working. fetching a joke${reset}" - joke=$(curl -s -H "Accept: application/json" --interface "$ipv6_address" https://icanhazdadjoke.com/ | jq -r .joke || true) + joke=$(curl -s -H "Accept: application/json" --interface "$ipv6_address" https://icanhazdadjoke.com/ | jq -r .joke) [[ -n "$joke" && "$joke" != "null" ]] && echo -e "${green}ipv6 joke: $joke${reset}" || echo -e "${red}failed to fetch a joke via ipv6${reset}" else echo -e "${red}ipv6 connectivity is not working for $interface. verify your routing and nat settings${reset}" From 06dd74ba349410362d642fe6feb6482ad6c2e81f Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:18:38 +0000 Subject: [PATCH 031/180] address comments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../nym-node-setup/network-tunnel-manager.sh | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 6d8626078e..daf3ed17bd 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -17,17 +17,17 @@ echo "checking for jq..." if command -v jq >/dev/null 2>&1; then echo "jq is already installed" # continue script execution -fi - -echo "jq not found, installing..." -apt-get update -y -DEBIAN_FRONTEND=noninteractive apt-get install -y jq - -if command -v jq >/dev/null 2>&1; then - echo "jq installed successfully" else - echo "failed to install jq" - exit 1 + echo "jq not found, installing..." + apt-get update -y + DEBIAN_FRONTEND=noninteractive apt-get install -y jq + + if command -v jq >/dev/null 2>&1; then + echo "jq installed successfully" + else + echo "failed to install jq" + exit 1 + fi fi ############################################################################### From c5971d0e9d0bb5dc3d8166f18022423cb966f91d Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:18:54 +0000 Subject: [PATCH 032/180] align space Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index daf3ed17bd..2251614470 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -518,7 +518,7 @@ apply_port_allowlist() { ["NDMP"]="10000" ["OpenPGP"]="11371" ["Monero"]="18080-18081" - ["MoneroRPC"]="18089" + ["MoneroRPC"]="18089" ["GoogleVoice"]="19294" ["EnsimControlPanel"]="19638" ["DarkFiTor"]="25551" From a6fe1b1de7713594b65750de42dda0d51a6045bb Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:32:40 +0100 Subject: [PATCH 033/180] fix logic to ensure to more robust --- .../nym-node-setup/network-tunnel-manager.sh | 79 +++++++++++++++++-- 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 2251614470..93229176c2 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -142,45 +142,109 @@ remove_duplicate_rules() { echo "detecting and removing duplicate rules for $interface in FORWARD and ${NYM_CHAIN}" - + # # ipv4 + # local rules_v4 rules_v4=$(iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" || true) if [[ -n "$rules_v4" ]]; then echo "processing ipv4 rules" + echo "$rules_v4" | sort | uniq | while read -r rule; do + local count count=$(echo "$rules_v4" | grep -Fx "$rule" | wc -l) + if [[ "$count" -gt 1 ]]; then echo "removing $((count - 1)) duplicate(s) of ipv4 rule: $rule" + for ((i=1; i/dev/null; then - iptables -t filter -D ${rule#-A } || break + local cleaned="${rule#-A }" + + # try exact match delete first + if iptables -t filter -C $cleaned 2>/dev/null; then + iptables -t filter -D $cleaned && continue + fi + + # fallback: locate rule in iptables -S + local match + match=$(iptables -S | grep -F -- "$cleaned" | head -n1 || true) + + if [[ -n "$match" ]]; then + # match looks like: -A FORWARD ...full rule... + local chain + chain=$(echo "$match" | awk '{print $2}') + + # find the rule index from iptables -L --line-numbers + local index + index=$(iptables -L "$chain" --line-numbers | grep -F "$interface" | awk 'NR==1{print $1}') + + if [[ -n "$index" ]]; then + iptables -D "$chain" "$index" 2>/dev/null || \ + echo "warning: failed to delete ipv4 duplicate via index ($chain $index)" + else + echo "warning: unable to locate ipv4 duplicate index for: $rule" + fi + else + echo "warning: could not match ipv4 duplicate rule reliably: $rule" fi done fi done + else echo "no ipv4 rules found for $interface to deduplicate" fi + # # ipv6 + # local rules_v6 rules_v6=$(ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" || true) if [[ -n "$rules_v6" ]]; then echo "processing ipv6 rules" + echo "$rules_v6" | sort | uniq | while read -r rule; do + local count count=$(echo "$rules_v6" | grep -Fx "$rule" | wc -l) + if [[ "$count" -gt 1 ]]; then echo "removing $((count - 1)) duplicate(s) of ipv6 rule: $rule" + for ((i=1; i/dev/null; then - ip6tables -t filter -D ${rule#-A } || break + local cleaned="${rule#-A }" + + # try exact delete first + if ip6tables -t filter -C $cleaned 2>/dev/null; then + ip6tables -t filter -D $cleaned && continue fi + + # fallback: find matching rule in ip6tables -S + local match + match=$(ip6tables -S | grep -F -- "$cleaned" | head -n1 || true) + + if [[ -n "$match" ]]; then + local chain + chain=$(echo "$match" | awk '{print $2}') + + local index + index=$(ip6tables -L "$chain" --line-numbers | grep -F "$interface" | awk 'NR==1{print $1}') + + if [[ -n "$index" ]]; then + ip6tables -D "$chain" "$index" 2>/dev/null || \ + echo "warning: failed to delete ipv6 duplicate via index ($chain $index)" + else + echo "warning: unable to locate ipv6 duplicate index for: $rule" + fi + else + echo "warning: could not match ipv6 duplicate rule reliably: $rule" + fi + done fi done + else echo "no ipv6 rules found for $interface to deduplicate" fi @@ -1017,7 +1081,6 @@ EOF ;; esac -# Only print when operation succeeded -if [ "$status" -eq 0 ]; then +if [ "${status:-1}" -eq 0 ]; then echo "operation ${cmd} completed" -fi \ No newline at end of file +fi From 04be5624fae8a8fea8b03c9acf9e07a5fd58f35c Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:41:42 +0100 Subject: [PATCH 034/180] ensure cars passing in a shell --- scripts/nym-node-setup/network-tunnel-manager.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 93229176c2..2f3d9b2424 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -151,7 +151,9 @@ remove_duplicate_rules() { if [[ -n "$rules_v4" ]]; then echo "processing ipv4 rules" - echo "$rules_v4" | sort | uniq | while read -r rule; do + while read -r rule; do + [[ -z "$rule" ]] && continue + local count count=$(echo "$rules_v4" | grep -Fx "$rule" | wc -l) @@ -171,11 +173,9 @@ remove_duplicate_rules() { match=$(iptables -S | grep -F -- "$cleaned" | head -n1 || true) if [[ -n "$match" ]]; then - # match looks like: -A FORWARD ...full rule... local chain chain=$(echo "$match" | awk '{print $2}') - # find the rule index from iptables -L --line-numbers local index index=$(iptables -L "$chain" --line-numbers | grep -F "$interface" | awk 'NR==1{print $1}') @@ -190,7 +190,7 @@ remove_duplicate_rules() { fi done fi - done + done < <(echo "$rules_v4" | sort | uniq) else echo "no ipv4 rules found for $interface to deduplicate" From 4e1228fff0ba7e7db54ec1e148557f50b0263f31 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:43:39 +0100 Subject: [PATCH 035/180] ensure cars passing in a shell --- .../nym-node-setup/network-tunnel-manager.sh | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 2f3d9b2424..eac8a4112a 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -625,21 +625,36 @@ EOF total_rules=$(grep "^ExitPolicy reject" "$POLICY_FILE" | grep -v "\*:\*" | wc -l || true) echo "processing $total_rules blocklist rules" - grep "^ExitPolicy reject" "$POLICY_FILE" | grep -v "\*:\*" | while read -r line; do + # + # main loop — no subshell, safe variable handling + # + while read -r line; do + [[ -z "$line" ]] && continue + local ip_range ip_range=$(echo "$line" | sed -E 's/ExitPolicy reject ([^:]+):.*/\1/') if [[ -n "$ip_range" ]]; then + + # ipv4 reject if ! iptables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT 2>/dev/null; then - iptables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT + iptables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT || \ + echo "warning: failed adding ipv4 reject for $ip_range" fi - if [[ "$ip_range" == *":"* ]] && ! ip6tables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT 2>/dev/null; then - ip6tables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT + + # ipv6 reject + if [[ "$ip_range" == *":"* ]]; then + if ! ip6tables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT 2>/dev/null; then + ip6tables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT || \ + echo "warning: failed adding ipv6 reject for $ip_range" + fi fi fi - done + + done < <(grep "^ExitPolicy reject" "$POLICY_FILE" | grep -v "\*:\*") } + add_default_reject_rule() { echo "ensuring default reject rule at end of ${NYM_CHAIN}" From 5627ada57e003123f89048f8535502156a71426c Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:49:02 +0000 Subject: [PATCH 036/180] address comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index eac8a4112a..312b7e0913 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -41,9 +41,17 @@ WG_INTERFACE="${WG_INTERFACE:-nymwg}" NETWORK_DEVICE="${NETWORK_DEVICE:-}" if [[ -z "$NETWORK_DEVICE" ]]; then NETWORK_DEVICE="$(ip -o route show default 2>/dev/null | awk '{print $5}' | head -n1 || true)" + # Validate that NETWORK_DEVICE is non-empty and looks like a network interface + if [[ -z "$NETWORK_DEVICE" || ! "$NETWORK_DEVICE" =~ ^[a-zA-Z0-9_-]+$ ]]; then + NETWORK_DEVICE="" + fi fi if [[ -z "$NETWORK_DEVICE" ]]; then NETWORK_DEVICE="$(ip -o route show default table all 2>/dev/null | awk '{print $5}' | head -n1 || true)" + # Validate that NETWORK_DEVICE is non-empty and looks like a network interface + if [[ -z "$NETWORK_DEVICE" || ! "$NETWORK_DEVICE" =~ ^[a-zA-Z0-9_-]+$ ]]; then + NETWORK_DEVICE="" + fi fi if [[ -z "$NETWORK_DEVICE" ]]; then echo "cannot determine uplink interface. set NETWORK_DEVICE or UPLINK_DEV" From 99b28b2b6f5ce146673a8ce21225936a4dad5eaa Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:49:32 +0000 Subject: [PATCH 037/180] address comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 312b7e0913..b4b21d6531 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -172,8 +172,8 @@ remove_duplicate_rules() { local cleaned="${rule#-A }" # try exact match delete first - if iptables -t filter -C $cleaned 2>/dev/null; then - iptables -t filter -D $cleaned && continue + if iptables -t filter -C "$cleaned" 2>/dev/null; then + iptables -t filter -D "$cleaned" && continue fi # fallback: locate rule in iptables -S From 91d0b7bdad7f8481b0fde4cb672783c44e2c0f41 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:49:49 +0000 Subject: [PATCH 038/180] address comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index b4b21d6531..529788599c 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -224,8 +224,8 @@ remove_duplicate_rules() { local cleaned="${rule#-A }" # try exact delete first - if ip6tables -t filter -C $cleaned 2>/dev/null; then - ip6tables -t filter -D $cleaned && continue + if ip6tables -t filter -C "$cleaned" 2>/dev/null; then + ip6tables -t filter -D "$cleaned" && continue fi # fallback: find matching rule in ip6tables -S From 239c6c701b83a6bac0cfb295ec8513ba21d07e3e Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:50:19 +0000 Subject: [PATCH 039/180] address comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 529788599c..79e97dd619 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -636,10 +636,9 @@ EOF # # main loop — no subshell, safe variable handling # + local ip_range while read -r line; do [[ -z "$line" ]] && continue - - local ip_range ip_range=$(echo "$line" | sed -E 's/ExitPolicy reject ([^:]+):.*/\1/') if [[ -n "$ip_range" ]]; then From 71e0c025c6d95641ab44247920f897c4dfdd26ca Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:50:42 +0000 Subject: [PATCH 040/180] address comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 79e97dd619..4609a29545 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -603,8 +603,9 @@ apply_port_allowlist() { ["Metadata"]="51830" ) + local port for service in "${!PORT_MAPPINGS[@]}"; do - local port="${PORT_MAPPINGS[$service]}" + port="${PORT_MAPPINGS[$service]}" echo "adding rules for $service (ports $port)" add_port_rules iptables "$port" "tcp" add_port_rules ip6tables "$port" "tcp" From 943b5fa8bc5d4ea9cf70e08f7c406bb707b07c36 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:51:01 +0000 Subject: [PATCH 041/180] address comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 4609a29545..b4f5c6eb26 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -783,9 +783,9 @@ test_port_range_rules() { ) local failures=0 + local start end for entry in "${port_ranges[@]}"; do IFS=':' read -r range proto name <<<"$entry" - local start end start=$(echo "$range" | cut -d'-' -f1) end=$(echo "$range" | cut -d'-' -f2) From 1525aed657cd318482eae5a4617490753ccaf1de Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:56:18 +0000 Subject: [PATCH 042/180] expand pattern to common naming conventions Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index b4f5c6eb26..e2a8b3d5d1 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -42,14 +42,14 @@ NETWORK_DEVICE="${NETWORK_DEVICE:-}" if [[ -z "$NETWORK_DEVICE" ]]; then NETWORK_DEVICE="$(ip -o route show default 2>/dev/null | awk '{print $5}' | head -n1 || true)" # Validate that NETWORK_DEVICE is non-empty and looks like a network interface - if [[ -z "$NETWORK_DEVICE" || ! "$NETWORK_DEVICE" =~ ^[a-zA-Z0-9_-]+$ ]]; then + if [[ -z "$NETWORK_DEVICE" || ! "$NETWORK_DEVICE" =~ ^[a-zA-Z0-9._-]+$ ]]; then NETWORK_DEVICE="" fi fi if [[ -z "$NETWORK_DEVICE" ]]; then NETWORK_DEVICE="$(ip -o route show default table all 2>/dev/null | awk '{print $5}' | head -n1 || true)" # Validate that NETWORK_DEVICE is non-empty and looks like a network interface - if [[ -z "$NETWORK_DEVICE" || ! "$NETWORK_DEVICE" =~ ^[a-zA-Z0-9_-]+$ ]]; then + if [[ -z "$NETWORK_DEVICE" || ! "$NETWORK_DEVICE" =~ ^[a-zA-Z0-9._-]+$ ]]; then NETWORK_DEVICE="" fi fi From aea74425258ea271a12c9a4b258a493d9257e587 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:56:52 +0000 Subject: [PATCH 043/180] add status Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index e2a8b3d5d1..0563510fc2 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -1107,3 +1107,4 @@ esac if [ "${status:-1}" -eq 0 ]; then echo "operation ${cmd} completed" fi +exit $status From 219f3af9675d4a6798a9dcceaea2013a50a5c83c Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:57:34 +0100 Subject: [PATCH 044/180] remove subshell --- .../nym-node-setup/network-tunnel-manager.sh | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 0563510fc2..e31051d825 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -635,34 +635,43 @@ EOF echo "processing $total_rules blocklist rules" # - # main loop — no subshell, safe variable handling + # SAFE non-subshell loop using a temp file # - local ip_range - while read -r line; do + local tmpfile + tmpfile=$(mktemp) + + grep "^ExitPolicy reject" "$POLICY_FILE" | grep -v "\*:\*" > "$tmpfile" + + local line ip_range + while IFS= read -r line; do [[ -z "$line" ]] && continue + ip_range=$(echo "$line" | sed -E 's/ExitPolicy reject ([^:]+):.*/\1/') if [[ -n "$ip_range" ]]; then # ipv4 reject if ! iptables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT 2>/dev/null; then - iptables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT || \ - echo "warning: failed adding ipv4 reject for $ip_range" + iptables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT \ + || echo "warning: failed adding ipv4 reject for $ip_range" fi # ipv6 reject if [[ "$ip_range" == *":"* ]]; then if ! ip6tables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT 2>/dev/null; then - ip6tables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT || \ - echo "warning: failed adding ipv6 reject for $ip_range" + ip6tables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT \ + || echo "warning: failed adding ipv6 reject for $ip_range" fi fi - fi - done < <(grep "^ExitPolicy reject" "$POLICY_FILE" | grep -v "\*:\*") + fi + done < "$tmpfile" + + rm -f "$tmpfile" } + add_default_reject_rule() { echo "ensuring default reject rule at end of ${NYM_CHAIN}" From 81fd37e5c039abce13d945e50bd26356df808d00 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:02:56 +0000 Subject: [PATCH 045/180] address comments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index e31051d825..6e0a0e4bff 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -630,18 +630,14 @@ ExitPolicy reject *:* EOF fi - local total_rules - total_rules=$(grep "^ExitPolicy reject" "$POLICY_FILE" | grep -v "\*:\*" | wc -l || true) - echo "processing $total_rules blocklist rules" - - # - # SAFE non-subshell loop using a temp file - # local tmpfile tmpfile=$(mktemp) grep "^ExitPolicy reject" "$POLICY_FILE" | grep -v "\*:\*" > "$tmpfile" + local total_rules + total_rules=$(wc -l < "$tmpfile") + echo "processing $total_rules blocklist rules" local line ip_range while IFS= read -r line; do [[ -z "$line" ]] && continue From de4fb6291c6d317deed3255db6fcd0696ce79266 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:03:50 +0000 Subject: [PATCH 046/180] address comments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 6e0a0e4bff..a2c160a587 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -170,10 +170,13 @@ remove_duplicate_rules() { for ((i=1; i/dev/null; then - iptables -t filter -D "$cleaned" && continue + if iptables -t filter -C "$chain" $rest 2>/dev/null; then + iptables -t filter -D "$chain" $rest && continue fi # fallback: locate rule in iptables -S From 8c799b29760c59a3c9d1055116beb009359ad5a7 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:04:34 +0000 Subject: [PATCH 047/180] address comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index a2c160a587..25e8ca69d7 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -225,10 +225,12 @@ remove_duplicate_rules() { for ((i=1; i/dev/null; then - ip6tables -t filter -D "$cleaned" && continue + if ip6tables -t filter -C "$chain" $rule_spec 2>/dev/null; then + ip6tables -t filter -D "$chain" $rule_spec && continue fi # fallback: find matching rule in ip6tables -S From ba0182058648e4aa577afb3d28b72649d27fd6db Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:05:10 +0000 Subject: [PATCH 048/180] address comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 25e8ca69d7..c09ce4cacf 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -653,7 +653,7 @@ EOF # ipv4 reject if ! iptables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT 2>/dev/null; then - iptables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT \ + iptables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT --reject-with icmp-port-unreachable \ || echo "warning: failed adding ipv4 reject for $ip_range" fi From cf8a399089bb1d09ef2f450a34ede3ccd10a84cb Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:07:20 +0100 Subject: [PATCH 049/180] remove subshell --- .../nym-node-setup/network-tunnel-manager.sh | 66 +++++++++++-------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index c09ce4cacf..72c891e791 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -159,54 +159,60 @@ remove_duplicate_rules() { if [[ -n "$rules_v4" ]]; then echo "processing ipv4 rules" - while read -r rule; do + # temp file to avoid subshell loops + local tmp4 + tmp4=$(mktemp) + + printf "%s\n" "$rules_v4" | sort | uniq > "$tmp4" + + local rule count cleaned chain rest match index + + while IFS= read -r rule; do [[ -z "$rule" ]] && continue - local count - count=$(echo "$rules_v4" | grep -Fx "$rule" | wc -l) + count=$(printf "%s\n" "$rules_v4" | grep -Fx "$rule" | wc -l) if [[ "$count" -gt 1 ]]; then echo "removing $((count - 1)) duplicate(s) of ipv4 rule: $rule" for ((i=1; i/dev/null; then iptables -t filter -D "$chain" $rest && continue fi - # fallback: locate rule in iptables -S - local match + # fallback: find exact match textually in iptables -S match=$(iptables -S | grep -F -- "$cleaned" | head -n1 || true) if [[ -n "$match" ]]; then - local chain chain=$(echo "$match" | awk '{print $2}') - local index index=$(iptables -L "$chain" --line-numbers | grep -F "$interface" | awk 'NR==1{print $1}') if [[ -n "$index" ]]; then iptables -D "$chain" "$index" 2>/dev/null || \ - echo "warning: failed to delete ipv4 duplicate via index ($chain $index)" + echo "warning: failed deleting ipv4 duplicate via index ($chain $index)" else echo "warning: unable to locate ipv4 duplicate index for: $rule" fi else - echo "warning: could not match ipv4 duplicate rule reliably: $rule" + echo "warning: could not reliably match ipv4 duplicate rule: $rule" fi done fi - done < <(echo "$rules_v4" | sort | uniq) + done < "$tmp4" + + rm -f "$tmp4" else echo "no ipv4 rules found for $interface to deduplicate" fi + # # ipv6 # @@ -216,47 +222,54 @@ remove_duplicate_rules() { if [[ -n "$rules_v6" ]]; then echo "processing ipv6 rules" - echo "$rules_v6" | sort | uniq | while read -r rule; do - local count - count=$(echo "$rules_v6" | grep -Fx "$rule" | wc -l) + local tmp6 + tmp6=$(mktemp) + + printf "%s\n" "$rules_v6" | sort | uniq > "$tmp6" + + local rule count cleaned chain rule_spec match index + + while IFS= read -r rule; do + [[ -z "$rule" ]] && continue + + count=$(printf "%s\n" "$rules_v6" | grep -Fx "$rule" | wc -l) if [[ "$count" -gt 1 ]]; then echo "removing $((count - 1)) duplicate(s) of ipv6 rule: $rule" for ((i=1; i/dev/null; then ip6tables -t filter -D "$chain" $rule_spec && continue fi - # fallback: find matching rule in ip6tables -S - local match + # fallback lookup in ip6tables -S match=$(ip6tables -S | grep -F -- "$cleaned" | head -n1 || true) if [[ -n "$match" ]]; then - local chain chain=$(echo "$match" | awk '{print $2}') - local index index=$(ip6tables -L "$chain" --line-numbers | grep -F "$interface" | awk 'NR==1{print $1}') if [[ -n "$index" ]]; then ip6tables -D "$chain" "$index" 2>/dev/null || \ - echo "warning: failed to delete ipv6 duplicate via index ($chain $index)" + echo "warning: failed deleting ipv6 duplicate via index ($chain $index)" else echo "warning: unable to locate ipv6 duplicate index for: $rule" fi else echo "warning: could not match ipv6 duplicate rule reliably: $rule" fi - done fi - done + + done < "$tmp6" + + rm -f "$tmp6" else echo "no ipv6 rules found for $interface to deduplicate" @@ -266,6 +279,7 @@ remove_duplicate_rules() { } + apply_iptables_rules() { local interface=$1 echo "applying iptables rules for $interface using uplink $NETWORK_DEVICE" From a38917cb9bb94c70c56e2996ecb10f3f33cfd5ef Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:22:34 +0000 Subject: [PATCH 050/180] address comments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 72c891e791..0e1cf0062c 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -181,8 +181,8 @@ remove_duplicate_rules() { rest=$(echo "$cleaned" | cut -d' ' -f2-) # try exact delete first - if iptables -t filter -C "$chain" $rest 2>/dev/null; then - iptables -t filter -D "$chain" $rest && continue + if iptables -t filter -C "$chain" "$rest" 2>/dev/null; then + iptables -t filter -D "$chain" "$rest" && continue fi # fallback: find exact match textually in iptables -S From 45e14a7fb1cf00c0452d404c1993d4c9e3bfc34e Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:22:57 +0000 Subject: [PATCH 051/180] address comments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 0e1cf0062c..b262cf4d86 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -243,8 +243,8 @@ remove_duplicate_rules() { rule_spec="${cleaned#"$chain" }" # try exact delete first - if ip6tables -t filter -C "$chain" $rule_spec 2>/dev/null; then - ip6tables -t filter -D "$chain" $rule_spec && continue + if ip6tables -t filter -C "$chain" "$rule_spec" 2>/dev/null; then + ip6tables -t filter -D "$chain" "$rule_spec" && continue fi # fallback lookup in ip6tables -S From 8ca6af7c867003a94137eb1bb111c5a8fe30fff5 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:23:18 +0000 Subject: [PATCH 052/180] syntax fix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index b262cf4d86..07ec842fda 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -144,7 +144,7 @@ remove_duplicate_rules() { local interface="$1" if [[ -z "$interface" ]]; then - echo "error: no interface specified. usage: $0 remove_duplicate_rules " + echo "Error: No interface specified. Usage: $0 remove_duplicate_rules " exit 1 fi From 76fc9f4a90ff6a71b5c370353f8816f4e237a9c7 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:23:38 +0000 Subject: [PATCH 053/180] syntax fix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/network-tunnel-manager.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 07ec842fda..f0a100c7e7 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -8,7 +8,7 @@ set -euo pipefail # safety: must run as root, jq ############################################################################### if [ "$(id -u)" -ne 0 ]; then - echo "this script must be run as root" + echo "This script must be run as root" exit 1 fi From 5ba181b11815a13cc8c66d03d099c4acd11fca6d Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:47:58 +0100 Subject: [PATCH 054/180] break into args --- .../nym-node-setup/network-tunnel-manager.sh | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index f0a100c7e7..fa32cf1cdd 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -159,10 +159,8 @@ remove_duplicate_rules() { if [[ -n "$rules_v4" ]]; then echo "processing ipv4 rules" - # temp file to avoid subshell loops local tmp4 tmp4=$(mktemp) - printf "%s\n" "$rules_v4" | sort | uniq > "$tmp4" local rule count cleaned chain rest match index @@ -180,17 +178,19 @@ remove_duplicate_rules() { chain=$(echo "$cleaned" | awk '{print $1}') rest=$(echo "$cleaned" | cut -d' ' -f2-) + # split "rest" into correct argv tokens + read -ra RULE_ARR <<<"$rest" + # try exact delete first - if iptables -t filter -C "$chain" "$rest" 2>/dev/null; then - iptables -t filter -D "$chain" "$rest" && continue + if iptables -t filter -C "$chain" "${RULE_ARR[@]}" 2>/dev/null; then + iptables -t filter -D "$chain" "${RULE_ARR[@]}" && continue fi - # fallback: find exact match textually in iptables -S + # fallback: find textual rule in iptables -S match=$(iptables -S | grep -F -- "$cleaned" | head -n1 || true) if [[ -n "$match" ]]; then chain=$(echo "$match" | awk '{print $2}') - index=$(iptables -L "$chain" --line-numbers | grep -F "$interface" | awk 'NR==1{print $1}') if [[ -n "$index" ]]; then @@ -202,6 +202,7 @@ remove_duplicate_rules() { else echo "warning: could not reliably match ipv4 duplicate rule: $rule" fi + done fi done < "$tmp4" @@ -224,7 +225,6 @@ remove_duplicate_rules() { local tmp6 tmp6=$(mktemp) - printf "%s\n" "$rules_v6" | sort | uniq > "$tmp6" local rule count cleaned chain rule_spec match index @@ -242,17 +242,19 @@ remove_duplicate_rules() { chain="${cleaned%% *}" rule_spec="${cleaned#"$chain" }" - # try exact delete first - if ip6tables -t filter -C "$chain" "$rule_spec" 2>/dev/null; then - ip6tables -t filter -D "$chain" "$rule_spec" && continue + # split rule_spec safely + read -ra RULE6_ARR <<<"$rule_spec" + + # try exact match delete + if ip6tables -t filter -C "$chain" "${RULE6_ARR[@]}" 2>/dev/null; then + ip6tables -t filter -D "$chain" "${RULE6_ARR[@]}" && continue fi - # fallback lookup in ip6tables -S + # fallback: lookup exact text in ip6tables -S match=$(ip6tables -S | grep -F -- "$cleaned" | head -n1 || true) if [[ -n "$match" ]]; then chain=$(echo "$match" | awk '{print $2}') - index=$(ip6tables -L "$chain" --line-numbers | grep -F "$interface" | awk 'NR==1{print $1}') if [[ -n "$index" ]]; then @@ -264,6 +266,7 @@ remove_duplicate_rules() { else echo "warning: could not match ipv6 duplicate rule reliably: $rule" fi + done fi @@ -280,6 +283,7 @@ remove_duplicate_rules() { + apply_iptables_rules() { local interface=$1 echo "applying iptables rules for $interface using uplink $NETWORK_DEVICE" From 766024be277b3bec62dc9b310c1fc297dee2995b Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:51:39 +0100 Subject: [PATCH 055/180] break into args --- .../nym-node-setup/network-tunnel-manager.sh | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index fa32cf1cdd..66180d67d3 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -164,11 +164,11 @@ remove_duplicate_rules() { printf "%s\n" "$rules_v4" | sort | uniq > "$tmp4" local rule count cleaned chain rest match index - while IFS= read -r rule; do [[ -z "$rule" ]] && continue - count=$(printf "%s\n" "$rules_v4" | grep -Fx "$rule" | wc -l) + # FIX: protect grep from rule content becoming flags + count=$(printf "%s\n" "$rules_v4" | grep -F -- "$rule" | wc -l) if [[ "$count" -gt 1 ]]; then echo "removing $((count - 1)) duplicate(s) of ipv4 rule: $rule" @@ -178,15 +178,12 @@ remove_duplicate_rules() { chain=$(echo "$cleaned" | awk '{print $1}') rest=$(echo "$cleaned" | cut -d' ' -f2-) - # split "rest" into correct argv tokens read -ra RULE_ARR <<<"$rest" - # try exact delete first if iptables -t filter -C "$chain" "${RULE_ARR[@]}" 2>/dev/null; then iptables -t filter -D "$chain" "${RULE_ARR[@]}" && continue fi - # fallback: find textual rule in iptables -S match=$(iptables -S | grep -F -- "$cleaned" | head -n1 || true) if [[ -n "$match" ]]; then @@ -202,9 +199,9 @@ remove_duplicate_rules() { else echo "warning: could not reliably match ipv4 duplicate rule: $rule" fi - done fi + done < "$tmp4" rm -f "$tmp4" @@ -214,6 +211,7 @@ remove_duplicate_rules() { fi + # # ipv6 # @@ -228,11 +226,11 @@ remove_duplicate_rules() { printf "%s\n" "$rules_v6" | sort | uniq > "$tmp6" local rule count cleaned chain rule_spec match index - while IFS= read -r rule; do [[ -z "$rule" ]] && continue - count=$(printf "%s\n" "$rules_v6" | grep -Fx "$rule" | wc -l) + # FIX: protect grep from interpreting rule as flags + count=$(printf "%s\n" "$rules_v6" | grep -F -- "$rule" | wc -l) if [[ "$count" -gt 1 ]]; then echo "removing $((count - 1)) duplicate(s) of ipv6 rule: $rule" @@ -242,19 +240,17 @@ remove_duplicate_rules() { chain="${cleaned%% *}" rule_spec="${cleaned#"$chain" }" - # split rule_spec safely read -ra RULE6_ARR <<<"$rule_spec" - # try exact match delete if ip6tables -t filter -C "$chain" "${RULE6_ARR[@]}" 2>/dev/null; then ip6tables -t filter -D "$chain" "${RULE6_ARR[@]}" && continue fi - # fallback: lookup exact text in ip6tables -S match=$(ip6tables -S | grep -F -- "$cleaned" | head -n1 || true) if [[ -n "$match" ]]; then chain=$(echo "$match" | awk '{print $2}') + index=$(ip6tables -L "$chain" --line-numbers | grep -F "$interface" | awk 'NR==1{print $1}') if [[ -n "$index" ]]; then @@ -266,7 +262,6 @@ remove_duplicate_rules() { else echo "warning: could not match ipv6 duplicate rule reliably: $rule" fi - done fi From 1559f6a9126d7e25a9ca7101da99f2d503754290 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:55:57 +0100 Subject: [PATCH 056/180] bugfix --- scripts/nym-node-setup/network-tunnel-manager.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 66180d67d3..66e412d629 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -154,7 +154,7 @@ remove_duplicate_rules() { # ipv4 # local rules_v4 - rules_v4=$(iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" || true) + rules_v4=$(iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep -F -- "$interface" || true) if [[ -n "$rules_v4" ]]; then echo "processing ipv4 rules" @@ -216,7 +216,7 @@ remove_duplicate_rules() { # ipv6 # local rules_v6 - rules_v6=$(ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" || true) + rules_v6=$(ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep -F -- "$interface" || true) if [[ -n "$rules_v6" ]]; then echo "processing ipv6 rules" From 4e5d88f64c702c04b127b4f3b45f4a4d249d059a Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:26:20 +0000 Subject: [PATCH 057/180] deleting to resolve merge confilict --- .../wireguard-exit-policy-manager.sh | 699 ------------------ 1 file changed, 699 deletions(-) delete mode 100644 scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh diff --git a/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh b/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh deleted file mode 100644 index f3288b6204..0000000000 --- a/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh +++ /dev/null @@ -1,699 +0,0 @@ -#!/bin/bash -# -# Nym Wireguard Exit Policy Manager -# Version: 1.0.0 -# -# This script manages iptables rules for Nym Wireguard exit policies -# Features: -# - Implements the Nym exit policy from official documentation -# - Makes rules persistent across reboots -# - Provides commands to inspect and manage rules -# - Groups rules logically for easier management -# - Integrates with existing Nym node configuration -# -# Usage: ./wireguard-exit-policy-manager.sh [command] - -set -e - -NETWORK_DEVICE=$(ip route show default | awk '/default/ {print $5}') -WG_INTERFACE="nymwg" -NYM_CHAIN="NYM-EXIT" -POLICY_FILE="/etc/nym/exit-policy.txt" -EXIT_POLICY_LOCATION="https://nymtech.net/.wellknown/network-requester/exit-policy.txt" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' - -add_port_rules() { - local chain="$1" - local port="$2" - local protocol="${3:-tcp}" - - # Check if the port contains a range - if [[ "$port" == *"-"* ]]; then - # Port range handling - add as a single rule with a range - local start_port=$(echo "$port" | cut -d'-' -f1) - local end_port=$(echo "$port" | cut -d'-' -f2) - - if ! $chain -C "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT 2>/dev/null; then - $chain -A "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT - echo -e " ${GREEN}Added: $NYM_CHAIN $protocol port range $start_port:$end_port${NC}" - fi - else - # Single port handling - if ! $chain -C "$NYM_CHAIN" -p "$protocol" --dport "$port" -j ACCEPT 2>/dev/null; then - $chain -A "$NYM_CHAIN" -p "$protocol" --dport "$port" -j ACCEPT - echo -e " ${GREEN}Added: $NYM_CHAIN $protocol port $port${NC}" - fi - fi -} - -install_dependencies() { - if ! dpkg -s iptables-persistent >/dev/null 2>&1; then - echo -e "${YELLOW}Installing iptables-persistent...${NC}" - apt-get update - DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent - echo -e "${GREEN}iptables-persistent installed.${NC}" - else - echo -e "${GREEN}iptables-persistent is already installed.${NC}" - fi - - # Check for other required dependencies - for cmd in iptables ip6tables ip grep sed awk wget curl; do - if ! command -v "$cmd" &>/dev/null; then - echo -e "${YELLOW}Installing $cmd...${NC}" - apt-get install -y "$cmd" - fi - done -} - -configure_ip_forwarding() { - echo -e "${YELLOW}Configuring IP forwarding...${NC}" - - # Remove any existing forwarding settings to avoid duplicates - sed -i "/^net.ipv6.conf.all.forwarding=/d" /etc/sysctl.conf - sed -i "/^net.ipv4.ip_forward=/d" /etc/sysctl.conf - - # Add forwarding settings - echo "net.ipv6.conf.all.forwarding=1" | tee -a /etc/sysctl.conf - echo "net.ipv4.ip_forward=1" | tee -a /etc/sysctl.conf - - # Apply changes - sysctl -p /etc/sysctl.conf - - # Verify settings - ipv4_forwarding=$(cat /proc/sys/net/ipv4/ip_forward) - ipv6_forwarding=$(cat /proc/sys/net/ipv6/conf/all/forwarding) - - if [ "$ipv4_forwarding" == "1" ] && [ "$ipv6_forwarding" == "1" ]; then - echo -e "${GREEN}IP forwarding configured successfully.${NC}" - else - echo -e "${RED}Failed to configure IP forwarding.${NC}" - exit 1 - fi -} - -create_nym_chain() { - echo -e "${YELLOW}Creating Nym exit policy chain...${NC}" - - # Check if the chain already exists - if iptables -L "$NYM_CHAIN" &>/dev/null; then - echo -e "${YELLOW}Chain $NYM_CHAIN already exists. Flushing it...${NC}" - iptables -F "$NYM_CHAIN" - else - echo -e "${YELLOW}Creating chain $NYM_CHAIN...${NC}" - iptables -N "$NYM_CHAIN" - fi - - # Do the same for IPv6 - if ip6tables -L "$NYM_CHAIN" &>/dev/null; then - echo -e "${YELLOW}Chain $NYM_CHAIN already exists in ip6tables. Flushing it...${NC}" - ip6tables -F "$NYM_CHAIN" - else - echo -e "${YELLOW}Creating chain $NYM_CHAIN in ip6tables...${NC}" - ip6tables -N "$NYM_CHAIN" - fi - - # Link it to the FORWARD chain if not already linked - if ! iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then - echo -e "${YELLOW}Linking $NYM_CHAIN to FORWARD chain...${NC}" - iptables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" - fi - - # Link IPv6 chain - if ! ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then - echo -e "${YELLOW}Linking $NYM_CHAIN to IPv6 FORWARD chain...${NC}" - ip6tables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" - fi -} - -setup_nat_rules() { - echo -e "${YELLOW}Setting up NAT rules...${NC}" - - # Check if NAT rule for IPv4 exists - 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 - echo -e "${GREEN}Added IPv4 NAT rule.${NC}" - else - echo -e "${GREEN}IPv4 NAT rule already exists.${NC}" - fi - - # Check if NAT rule for IPv6 exists - 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 - echo -e "${GREEN}Added IPv6 NAT rule.${NC}" - else - echo -e "${GREEN}IPv6 NAT rule already exists.${NC}" - fi - - # Setup forwarding rules for Wireguard interface - if ! iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null; then - iptables -A FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT - echo -e "${GREEN}Added IPv4 forwarding rule (WG → Internet).${NC}" - fi - - if ! iptables -C FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then - iptables -A FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT - echo -e "${GREEN}Added IPv4 forwarding rule (Internet → WG for established connections).${NC}" - fi - - # IPv6 forwarding rules - if ! ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null; then - ip6tables -A FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT - echo -e "${GREEN}Added IPv6 forwarding rule (WG → Internet).${NC}" - fi - - if ! ip6tables -C FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then - ip6tables -A FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT - echo -e "${GREEN}Added IPv6 forwarding rule (Internet → WG for established connections).${NC}" - fi -} - -configure_dns_and_icmp() { - echo -e "${YELLOW}Configuring DNS and ICMP rules...${NC}" - - # ICMP rules for ping - if ! iptables -C INPUT -p icmp --icmp-type echo-request -j ACCEPT 2>/dev/null; then - iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT - echo -e "${GREEN}Added IPv4 ICMP rule (allow ping requests).${NC}" - fi - - if ! iptables -C OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT 2>/dev/null; then - iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT - echo -e "${GREEN}Added IPv4 ICMP rule (allow ping replies).${NC}" - fi - - # ICMPv6 rules for ping6 - if ! ip6tables -C INPUT -p ipv6-icmp -j ACCEPT 2>/dev/null; then - ip6tables -A INPUT -p ipv6-icmp -j ACCEPT - echo -e "${GREEN}Added IPv6 ICMP rule (allow ping6).${NC}" - fi - - # DNS rules - if ! iptables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null; then - iptables -A INPUT -p udp --dport 53 -j ACCEPT - echo -e "${GREEN}Added IPv4 DNS rule (UDP).${NC}" - fi - - if ! iptables -C INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null; then - iptables -A INPUT -p tcp --dport 53 -j ACCEPT - echo -e "${GREEN}Added IPv4 DNS rule (TCP).${NC}" - fi - - # IPv6 DNS rules - if ! ip6tables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null; then - ip6tables -A INPUT -p udp --dport 53 -j ACCEPT - echo -e "${GREEN}Added IPv6 DNS rule (UDP).${NC}" - fi - - if ! ip6tables -C INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null; then - ip6tables -A INPUT -p tcp --dport 53 -j ACCEPT - echo -e "${GREEN}Added IPv6 DNS rule (TCP).${NC}" - fi -} - -# Apply Spamhaus blocklist from the Nym exit policy -apply_spamhaus_blocklist() { - echo -e "${YELLOW}Applying Spamhaus blocklist...${NC}" - - # Create directory if not exists - mkdir -p "$(dirname "$POLICY_FILE")" - - # Try to download the policy file - echo -e "${YELLOW}Downloading exit policy from $EXIT_POLICY_LOCATION${NC}" - if ! wget -q "$EXIT_POLICY_LOCATION" -O "$POLICY_FILE" 2>/dev/null; then - echo -e "${RED}Failed to download exit policy. Using minimal blocklist.${NC}" - - # Create a minimal policy file with a few entries - cat >"$POLICY_FILE" </dev/null; then - iptables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT - fi - - # Apply IPv6 rules for IPv6 addresses - if [[ "$ip_range" == *":"* ]] && ! ip6tables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT 2>/dev/null; then - ip6tables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT - fi - fi - done - - echo -e "${GREEN}Blocklist applied successfully.${NC}" -} - -add_default_reject_rule() { - echo -e "${YELLOW}Adding default reject rule...${NC}" - - # First remove any existing plain reject rules (without specific destinations) - iptables -D "$NYM_CHAIN" -j REJECT 2>/dev/null || true - iptables -D "$NYM_CHAIN" -j REJECT --reject-with icmp-port-unreachable 2>/dev/null || true - ip6tables -D "$NYM_CHAIN" -j REJECT 2>/dev/null || true - ip6tables -D "$NYM_CHAIN" -j REJECT --reject-with icmp6-port-unreachable 2>/dev/null || true - - # Add the default catch-all reject rule (must be the last rule in the chain) - iptables -A "$NYM_CHAIN" -j REJECT --reject-with icmp-port-unreachable - ip6tables -A "$NYM_CHAIN" -j REJECT --reject-with icmp6-port-unreachable - - echo -e "${GREEN}Default reject rule added successfully.${NC}" -} - -apply_port_allowlist() { - echo -e "${YELLOW}Applying allowed ports...${NC}" - - # Insert DNS rules at the very beginning of NYM-EXIT chain, this ensures DNS queries are never blocked by REJECT rules - echo -e "${YELLOW}Ensuring DNS is at the beginning of $NYM_CHAIN...${NC}" - - iptables -I "$NYM_CHAIN" 1 -p udp --dport 53 -j ACCEPT - iptables -I "$NYM_CHAIN" 2 -p tcp --dport 53 -j ACCEPT - ip6tables -I "$NYM_CHAIN" 1 -p udp --dport 53 -j ACCEPT - ip6tables -I "$NYM_CHAIN" 2 -p tcp --dport 53 -j ACCEPT - - # ICMP is needed because the probe tests DNS by pinging hostnames - iptables -I "$NYM_CHAIN" 3 -p icmp --icmp-type echo-request -j ACCEPT - iptables -I "$NYM_CHAIN" 4 -p icmp --icmp-type echo-reply -j ACCEPT - ip6tables -I "$NYM_CHAIN" 3 -p ipv6-icmp -j ACCEPT - - echo -e "${GREEN}✓ DNS and ICMP rules inserted at beginning of $NYM_CHAIN${NC}" - - # Dictionary of services and their ports - declare -A PORT_MAPPINGS=( - ["FTP"]="20-21" - ["SSH"]="22" - ["WHOIS"]="43" - ["DNS"]="53" - ["Finger"]="79" - ["HTTP"]="80-81" - ["Kerberos"]="88" - ["POP3"]="110" - ["NTP"]="123" - ["IMAP"]="143" - ["IMAP3"]="220" - ["LDAP"]="389" - ["HTTPS"]="443" - ["SMBWindowsFileShare"]="445" - ["Kpasswd"]="464" - ["RTSP"]="554" - ["LDAPS"]="636" - ["SILC"]="706" - ["KerberosAdmin"]="749" - ["DNSOverTLS"]="853" - ["Rsync"]="873" - ["VMware"]="902-904" - ["RemoteHTTPS"]="981" - ["FTPOverTLS"]="989-990" - ["NetnewsAdmin"]="991" - ["TelnetOverTLS"]="992" - ["IMAPOverTLS"]="993" - ["POP3OverTLS"]="995" - ["OpenVPN"]="1194" - ["WireGuardPeer"]="51820-51822" - ["QTServerAdmin"]="1220" - ["PKTKRB"]="1293" - ["MSSQL"]="1433" - ["VLSILicenseManager"]="1500" - ["OracleDB"]="1521" - ["Sametime"]="1533" - ["GroupWise"]="1677" - ["PPTP"]="1723" - ["RTSPAlt"]="1755" - ["MSNP"]="1863" - ["NFS"]="2049" - ["CPanel"]="2082-2083" - ["GNUnet"]="2086-2087" - ["NBX"]="2095-2096" - ["Zephyr"]="2102-2104" - ["XboxLive"]="3074" - ["MySQL"]="3306" - ["SVN"]="3690" - ["RWHOIS"]="4321" - ["Virtuozzo"]="4643" - ["RTPVOIP"]="5000-5005" - ["MMCC"]="5050" - ["ICQ"]="5190" - ["XMPP"]="5222-5223" - ["AndroidMarket"]="5228" - ["PostgreSQL"]="5432" - ["MongoDBDefault"]="27017" - ["Electrum"]="8082" - ["SimplifyMedia"]="8087-8088" - ["Zcash"]="8232-8233" - ["Bitcoin"]="8332-8333" - ["HTTPSALT"]="8443" - ["TeamSpeak"]="8767" - ["MQTTS"]="8883" - ["HTTPProxy"]="8888" - ["TorORPort"]="9001" - ["TorDirPort"]="9030" - ["Tari"]="9053" - ["Gaming"]="9339" - ["Git"]="9418" - ["HTTPSALT2"]="9443" - ["Lightning"]="9735" - ["NDMP"]="10000" - ["OpenPGP"]="11371" - ["Monero"]="18080-18081" - ["MoneroRPC"]="18089" - ["GoogleVoice"]="19294" - ["EnsimControlPanel"]="19638" - ["DarkFiTor"]="25551" - ["Minecraft"]="25565" - ["DarkFi"]="26661" - ["Steam"]="27000-27050" - ["ElectrumSSL"]="50002" - ["MOSH"]="60000-61000" - ["Mumble"]="64738" - ["Metadata"]="51830" - ) - - # Add TCP and UDP rules for each allowed port - for service in "${!PORT_MAPPINGS[@]}"; do - port="${PORT_MAPPINGS[$service]}" - echo -e "${YELLOW}Adding rules for $service (Port: $port)${NC}" - - # Add both TCP and UDP rules for all services - add_port_rules iptables "$port" "tcp" - add_port_rules ip6tables "$port" "tcp" - add_port_rules iptables "$port" "udp" - add_port_rules ip6tables "$port" "udp" - done - - echo -e "${GREEN}Port allowlist applied successfully.${NC}" -} - -safe_iptables_rule_remove() { - local chain="$1" - local table="${2:-filter}" - local interface="$3" - - # Remove rule if it exists - if iptables -t "$table" -C "$chain" -o "$interface" -j "$NYM_CHAIN" 2>/dev/null; then - iptables -t "$table" -D "$chain" -o "$interface" -j "$NYM_CHAIN" - fi -} - -safe_ip6tables_rule_remove() { - local chain="$1" - local table="${2:-filter}" - local interface="$3" - - # Remove rule if it exists - if ip6tables -t "$table" -C "$chain" -o "$interface" -j "$NYM_CHAIN" 2>/dev/null; then - ip6tables -t "$table" -D "$chain" -o "$interface" -j "$NYM_CHAIN" - fi -} - -clear_rules() { - echo -e "${YELLOW}Clearing Nym exit policy rules...${NC}" - - # Flush all rules in the NYM-EXIT chain - iptables -F "$NYM_CHAIN" 2>/dev/null || true - ip6tables -F "$NYM_CHAIN" 2>/dev/null || true - - # Remove the chain from FORWARD if it exists - iptables -D FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null || true - iptables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null || true - ip6tables -D FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null || true - ip6tables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null || true - - # Delete the chains - iptables -X "$NYM_CHAIN" 2>/dev/null || true - ip6tables -X "$NYM_CHAIN" 2>/dev/null || true - - echo -e "${GREEN}Nym exit policy rules cleared successfully.${NC}" -} - -remove_duplicate_rules() { - local interface="$1" - - if [[ -z "$interface" ]]; then - echo -e "${RED}Error: No interface specified. Usage: $0 remove-duplicates ${NC}" >&2 - exit 1 - fi - - echo -e "${YELLOW}Detecting and removing duplicate rules for $interface...${NC}" - - # Verbose duplicate rule detection for IPv4 - echo -e "${YELLOW}Checking IPv4 duplicate rules:${NC}" - iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq -d && { - echo -e "${RED}Duplicate IPv4 rules found! Removing...${NC}" - # Remove duplicates by saving unique rules - iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq | while read -r rule; do - # Carefully remove duplicates - full_rule=$(echo "$rule" | sed 's/^-A/iptables -D/') - eval "$full_rule" 2>/dev/null - done - } || echo -e "${GREEN}No duplicate IPv4 rules found.${NC}" - - # Verbose duplicate rule detection for IPv6 - echo -e "${YELLOW}Checking IPv6 duplicate rules:${NC}" - ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq -d && { - echo -e "${RED}Duplicate IPv6 rules found! Removing...${NC}" - # Remove duplicates by saving unique rules - ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq | while read -r rule; do - # Carefully remove duplicates - full_rule=$(echo "$rule" | sed 's/^-A/ip6tables -D/') - eval "$full_rule" 2>/dev/null - done - } || echo -e "${GREEN}No duplicate IPv6 rules found.${NC}" - - # Additional verification - echo -e "\n${YELLOW}Rule verification:${NC}" - echo "IPv4 Rules:" - iptables -L FORWARD -v -n | grep "$interface" - echo "IPv6 Rules:" - ip6tables -L FORWARD -v -n | grep "$interface" - - echo -e "${GREEN}Duplicate rule removal process completed.${NC}" -} - -save_rules() { - echo -e "${YELLOW}Saving iptables rules to make them persistent...${NC}" - - if [ -d "/etc/iptables" ]; then - # For Debian/Ubuntu with iptables-persistent - iptables-save >/etc/iptables/rules.v4 - ip6tables-save >/etc/iptables/rules.v6 - echo -e "${GREEN}Rules saved to /etc/iptables/rules.v4 and /etc/iptables/rules.v6${NC}" - else - # Fallback method - iptables-save >/etc/iptables.rules - ip6tables-save >/etc/ip6tables.rules - echo -e "${GREEN}Rules saved to /etc/iptables.rules and /etc/ip6tables.rules${NC}" - - # Add loading script to rc.local if it doesn't exist - if [ ! -f "/etc/network/if-pre-up.d/iptables" ]; then - cat >/etc/network/if-pre-up.d/iptables </dev/null; then - echo -e "${RED}WARNING: Wireguard interface $WG_INTERFACE not found!${NC}" - return 1 - fi - - # Interface details - echo -e "\n${YELLOW}Interface Details:${NC}" - ip link show "$WG_INTERFACE" - - # IP Addresses - echo -e "\n${YELLOW}IP Addresses:${NC}" - ip -4 addr show dev "$WG_INTERFACE" - ip -6 addr show dev "$WG_INTERFACE" - - # Iptables Chain Status - echo -e "\n${YELLOW}Iptables Chains:${NC}" - { - echo "IPv4 Chain:" - iptables -L "$NYM_CHAIN" -n -v - echo -e "\nIPv6 Chain:" - ip6tables -L "$NYM_CHAIN" -n -v - } || echo "One or both chains not found" - - # Forwarding Status - echo -e "\n${YELLOW}IP Forwarding:${NC}" - echo "IPv4: $(cat /proc/sys/net/ipv4/ip_forward)" - echo "IPv6: $(cat /proc/sys/net/ipv6/conf/all/forwarding)" -} - -test_connectivity() { - echo -e "${YELLOW}Testing connectivity through $WG_INTERFACE...${NC}" - - # More comprehensive interface check - interface_info=$(ip link show "$WG_INTERFACE" 2>/dev/null) - - if [ -z "$interface_info" ]; then - echo -e "${RED}Interface $WG_INTERFACE not found!${NC}" - return 1 - fi - - # Check for multiple possible interface states - if ! echo "$interface_info" | grep -qE "state (UP|UNKNOWN|DORMANT)"; then - echo -e "${RED}Interface $WG_INTERFACE is not in an active state!${NC}" - echo "$interface_info" - return 1 - fi - - # Detailed interface information - echo -e "${GREEN}Interface Details:${NC}" - echo "$interface_info" - - # Get IP addresses with more robust method - ipv4_address=$(ip -4 addr show dev "$WG_INTERFACE" | grep -oP '(?<=inet\s)\d+\.\d+\.\d+\.\d+/\d+' | cut -d'/' -f1 | head -n1) - ipv6_address=$(ip -6 addr show dev "$WG_INTERFACE" scope global | grep -oP '(?<=inet6\s)[0-9a-f:]+/\d+' | cut -d'/' -f1 | head -n1) - - echo -e "${GREEN}IPv4 Address:${NC} ${ipv4_address:-Not found}" - echo -e "${GREEN}IPv6 Address:${NC} ${ipv6_address:-Not found}" - - # Connectivity tests - if [[ -n "$ipv4_address" ]]; then - echo -e "${YELLOW}Testing IPv4 connectivity from $ipv4_address...${NC}" - - # Ping test - if timeout 5 ping -c 3 -I "$ipv4_address" 8.8.8.8 >/dev/null 2>&1; then - echo -e "${GREEN}IPv4 connectivity to 8.8.8.8: Success${NC}" - else - echo -e "${RED}IPv4 connectivity to 8.8.8.8: Failed${NC}" - fi - - # DNS resolution test - if timeout 5 ping -c 3 -I "$ipv4_address" google.com >/dev/null 2>&1; then - echo -e "${GREEN}IPv4 DNS resolution: Success${NC}" - else - echo -e "${RED}IPv4 DNS resolution: Failed${NC}" - fi - - # HTTP(S) connectivity test - if command -v curl &>/dev/null; then - if timeout 5 curl -s --interface "$ipv4_address" -o /dev/null -w "%{http_code}" https://www.google.com | grep -q "200"; then - echo -e "${GREEN}IPv4 HTTPS connectivity: Success${NC}" - else - echo -e "${RED}IPv4 HTTPS connectivity: Failed${NC}" - fi - fi - else - echo -e "${RED}No IPv4 address configured on $WG_INTERFACE${NC}" - fi - - # Similar tests for IPv6 if available - if [[ -n "$ipv6_address" ]]; then - echo -e "${YELLOW}Testing IPv6 connectivity from $ipv6_address...${NC}" - - if timeout 5 ping6 -c 3 -I "$ipv6_address" 2001:4860:4860::8888 >/dev/null 2>&1; then - echo -e "${GREEN}IPv6 connectivity to Google DNS: Success${NC}" - else - echo -e "${RED}IPv6 connectivity to Google DNS: Failed${NC}" - fi - - if timeout 5 ping6 -c 3 -I "$ipv6_address" google.com >/dev/null 2>&1; then - echo -e "${GREEN}IPv6 DNS resolution: Success${NC}" - else - echo -e "${RED}IPv6 DNS resolution: Failed${NC}" - fi - - if command -v curl &>/dev/null; then - if timeout 5 curl -s --interface "$ipv6_address" -o /dev/null -w "%{http_code}" https://www.google.com | grep -q "200"; then - echo -e "${GREEN}IPv6 HTTPS connectivity: Success${NC}" - else - echo -e "${RED}IPv6 HTTPS connectivity: Failed${NC}" - fi - fi - else - echo -e "${YELLOW}No IPv6 address configured on $WG_INTERFACE${NC}" - fi - - echo -e "${GREEN}Connectivity tests completed.${NC}" -} - -main() { - # Check for root privileges - if [ "$(id -u)" -ne 0 ]; then - echo -e "${RED}This script must be run as root${NC}" >&2 - exit 1 - fi - - # Parse command-line arguments - case "$1" in - install) - install_dependencies - configure_ip_forwarding - create_nym_chain - setup_nat_rules - configure_dns_and_icmp - # Apply allowlist first (so DNS and other allowed ports are not blocked) - apply_port_allowlist - # Then apply blocklist (aka the REJECT rules) - apply_spamhaus_blocklist - add_default_reject_rule - save_rules - echo -e "${GREEN}Nym exit policy installed successfully.${NC}" - ;; - status) - show_status - ;; - test) - test_connectivity - ;; - clear) - clear_rules - echo -e "${GREEN}Nym exit policy rules cleared.${NC}" - ;; - remove-duplicates) - remove_duplicate_rules "$2" - ;; - help | --help | -h) - echo "Usage: $0 [command]" - echo "" - echo "Commands:" - echo " install Install and configure Nym exit policy" - echo " status Show current Nym exit policy status" - echo " test Test connectivity through Wireguard interface" - echo " clear Remove all Nym exit policy rules" - echo " remove-duplicates Remove duplicate iptables rules for an interface" - echo " help Show this help message" - ;; - *) - echo -e "${RED}Invalid command. Use '$0 help' for usage information.${NC}" >&2 - exit 1 - ;; - esac -} - -main "$@" From 0fe863c8895eb40af855bcc63f123a1233a6530b Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:27:04 +0000 Subject: [PATCH 058/180] delete to resolve merge conflict --- .../exit-policy-tests.sh | 308 ------------------ 1 file changed, 308 deletions(-) delete mode 100644 scripts/wireguard-exit-policy/exit-policy-tests.sh diff --git a/scripts/wireguard-exit-policy/exit-policy-tests.sh b/scripts/wireguard-exit-policy/exit-policy-tests.sh deleted file mode 100644 index 0734f19318..0000000000 --- a/scripts/wireguard-exit-policy/exit-policy-tests.sh +++ /dev/null @@ -1,308 +0,0 @@ -#!/bin/bash -# Nym Exit Policy Verification Unit Tests - -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[0;33m' -NC='\033[0m' - -NYM_CHAIN="NYM-EXIT" -WG_INTERFACE="nymwg" - -check_port_range_rules() { - local port_range="$1" - local protocol="${2:-tcp}" - local chain="${3:-$NYM_CHAIN}" - - # Extract start and end ports - local start_port=$(echo "$port_range" | cut -d'-' -f1) - local end_port=$(echo "$port_range" | cut -d'-' -f2) - - if iptables -t filter -C "$chain" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT 2>/dev/null; then - echo -e "${GREEN}✓ Rule exists: $chain $protocol port range $start_port:$end_port${NC}" - return 0 - else - echo -e "${RED}✗ Rule missing: $chain $protocol port range $start_port:$end_port${NC}" - - echo -e "${YELLOW}Dumping all rules in $chain:${NC}" - iptables -L "$chain" -n | grep "$protocol" - - return 1 - fi -} - -# Test port range rules -test_port_range_rules() { - echo -e "${YELLOW}Testing Port Range Rules...${NC}" - - # Select the essential port ranges for testing - local port_ranges=( - "20-21:tcp:FTP" - "80-81:tcp:HTTP" - "2082-2083:tcp:CPanel" - "5222-5223:tcp:XMPP" - "27000-27050:tcp:Steam (sampling)" - "989-990:tcp:FTP over TLS" - "5000-5005:tcp:RTP/VoIP" - "8087-8088:tcp:Simplify Media" - "8232-8233:tcp:Zcash" - "8332-8333:tcp:Bitcoin" - "18080-18081:tcp:Monero" - ) - - local total_failures=0 - - for range in "${port_ranges[@]}"; do - IFS=':' read -r port_range protocol service <<< "$range" - - # Extract start and end ports - local start_port=$(echo "$port_range" | cut -d'-' -f1) - local end_port=$(echo "$port_range" | cut -d'-' -f2) - - echo -e "${YELLOW}Testing $service $protocol port range $port_range${NC}" - - if iptables -t filter -C "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT 2>/dev/null; then - echo -e "${GREEN}✓ Rule exists: $NYM_CHAIN $protocol port range $start_port:$end_port${NC}" - else - echo -e "${RED}✗ Rule missing: $NYM_CHAIN $protocol port range $start_port:$end_port${NC}" - ((total_failures++)) - - echo -e "${YELLOW}Existing rules for protocol $protocol:${NC}" - iptables -L "$NYM_CHAIN" -n | grep "$protocol" - fi - done - - if [ $total_failures -eq 0 ]; then - return 0 - else - return 1 - fi -} - -test_critical_services() { - echo -e "${YELLOW}Testing Critical Service Rules...${NC}" - - local tcp_services=( - 22 # SSH - 53 # DNS - 443 # HTTPS - 853 # DNS over TLS - 1194 # OpenVPN - ) - - local udp_services=( - 53 # DNS - 123 # NTP - 1194 # OpenVPN - ) - - local failures=0 - - # Test TCP services - for port in "${tcp_services[@]}"; do - local rule_found=false - - # First check for exact match - if iptables -t filter -C "$NYM_CHAIN" -p tcp --dport "$port" -j ACCEPT 2>/dev/null; then - echo -e "${GREEN}✓ Rule exists: NYM-EXIT tcp port $port${NC}" - rule_found=true - else - # If not found as exact port, search for it in port ranges - # This checks if the port is covered by any range rule - if iptables-save | grep -E "^-A $NYM_CHAIN.*tcp.*dpts:" | grep -qP "dpts:(\d+:)?$port(:|\d+)" || \ - iptables-save | grep -E "^-A $NYM_CHAIN.*tcp.*dpts:" | grep -qP "dpts:$port:"; then - echo -e "${GREEN}✓ Rule exists: NYM-EXIT tcp port $port (covered by a range rule)${NC}" - rule_found=true - else - echo -e "${RED}✗ Rule missing: NYM-EXIT tcp port $port${NC}" - ((failures++)) - fi - fi - done - - # Test UDP services - similar approach - for port in "${udp_services[@]}"; do - local rule_found=false - - if iptables -t filter -C "$NYM_CHAIN" -p udp --dport "$port" -j ACCEPT 2>/dev/null; then - echo -e "${GREEN}✓ Rule exists: NYM-EXIT udp port $port${NC}" - rule_found=true - else - # If not found as exact port, search for it in port ranges - if iptables-save | grep -E "^-A $NYM_CHAIN.*udp.*dpts:" | grep -qP "dpts:(\d+:)?$port(:|\d+)" || \ - iptables-save | grep -E "^-A $NYM_CHAIN.*udp.*dpts:" | grep -qP "dpts:$port:"; then - echo -e "${GREEN}✓ Rule exists: NYM-EXIT udp port $port (covered by a range rule)${NC}" - rule_found=true - else - echo -e "${RED}✗ Rule missing: NYM-EXIT udp port $port${NC}" - ((failures++)) - fi - fi - done - - echo -e "${YELLOW}Relevant existing rules for HTTP (port 80):${NC}" - iptables-save | grep -E "$NYM_CHAIN.*tcp" | grep -E "(dpt|dpts):.*80" - - return $failures -} - -# Test that the exit policy chain is correctly wired up to FORWARD chain -test_forward_chain_hook() { - echo -e "${YELLOW}Testing FORWARD Chain Hook...${NC}" - - local failures=0 - NETWORK_DEVICE=$(ip route show default | awk '/default/ {print $5}') - - if [[ -z "$NETWORK_DEVICE" ]]; then - echo -e "${RED}✗ Could not determine network device${NC}" - return 1 - fi - - # (incoming from nymwg, outgoing to network device) - if iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then - echo -e "${GREEN}✓ IPv4 FORWARD hook exists with correct direction: -i $WG_INTERFACE -o $NETWORK_DEVICE${NC}" - else - echo -e "${RED}✗ IPv4 FORWARD hook missing or has wrong direction${NC}" - echo -e "${YELLOW} Expected: -i $WG_INTERFACE -o $NETWORK_DEVICE -j $NYM_CHAIN${NC}" - - # Check if wrong direction exists - if iptables -C FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null; then - echo -e "${RED}✗ WRONG DIRECTION FOUND: -o $WG_INTERFACE (this matches return traffic, not client egress!)${NC}" - fi - - # Show what actually exists - specifically look for jump rules to NYM-EXIT - echo -e "${YELLOW} Current FORWARD rules that jump to $NYM_CHAIN:${NC}" - iptables -L FORWARD -n -v --line-numbers | grep -E "$NYM_CHAIN" || echo " (none found - exit policy chain is not hooked to FORWARD!)" - echo -e "${YELLOW} All FORWARD rules with $WG_INTERFACE (for reference):${NC}" - iptables -L FORWARD -n -v --line-numbers | grep -E "$WG_INTERFACE" | head -5 || echo " (none found)" - ((failures++)) - fi - - # Check IPv6 - if ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then - echo -e "${GREEN}✓ IPv6 FORWARD hook exists with correct direction: -i $WG_INTERFACE -o $NETWORK_DEVICE${NC}" - else - echo -e "${RED}✗ IPv6 FORWARD hook missing or has wrong direction${NC}" - echo -e "${YELLOW} Expected: -i $WG_INTERFACE -o $NETWORK_DEVICE -j $NYM_CHAIN${NC}" - - # Check if wrong direction exists - if ip6tables -C FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null; then - echo -e "${RED}✗ WRONG DIRECTION FOUND: -o $WG_INTERFACE (this matches return traffic, not client egress!)${NC}" - fi - - # Show what actually exists - specifically look for jump rules to NYM-EXIT - echo -e "${YELLOW} Current IPv6 FORWARD rules that jump to $NYM_CHAIN:${NC}" - ip6tables -L FORWARD -n -v --line-numbers | grep -E "$NYM_CHAIN" || echo " (none found - exit policy chain is not hooked to FORWARD!)" - echo -e "${YELLOW} All IPv6 FORWARD rules with $WG_INTERFACE (for reference):${NC}" - ip6tables -L FORWARD -n -v --line-numbers | grep -E "$WG_INTERFACE" | head -5 || echo " (none found)" - ((failures++)) - fi - - # Check rule position (should be before UFW rules) - local rule_num=$(iptables -L FORWARD -n --line-numbers -v | awk -v chain="$NYM_CHAIN" -v in="$WG_INTERFACE" -v out="$NETWORK_DEVICE" ' - $0 ~ chain && $0 ~ in && $0 ~ out {print $1; exit} - ') - if [[ -n "$rule_num" ]]; then - if [[ $rule_num -le 5 ]]; then - echo -e "${GREEN}✓ Rule is early in FORWARD chain (position #$rule_num) - good for UFW compatibility${NC}" - else - echo -e "${YELLOW}⚠ Rule is later in FORWARD chain (position #$rule_num) - may conflict with UFW${NC}" - fi - fi - - return $failures -} - -# Verify default reject rule exists -test_default_reject_rule() { - echo -e "${YELLOW}This test takes some time, do not quit the process${NC}" - echo - echo -e "${YELLOW}Testing Default Reject Rule...${NC}" - - # Try different patterns to detect the reject rule - if iptables -L "$NYM_CHAIN" | grep -q "REJECT.*all.*anywhere.*anywhere.*reject-with"; then - echo -e "${GREEN}✓ Default REJECT rule exists${NC}" - return 0 - elif iptables -L "$NYM_CHAIN" | grep -q "REJECT.*all -- .*everywhere.*everywhere"; then - echo -e "${GREEN}✓ Default REJECT rule exists${NC}" - return 0 - elif iptables -L "$NYM_CHAIN" | grep -q "REJECT.*all.*0.0.0.0/0.*0.0.0.0/0"; then - echo -e "${GREEN}✓ Default REJECT rule exists${NC}" - return 0 - elif iptables -n -L "$NYM_CHAIN" | grep -qE "REJECT.*all.*0.0.0.0/0.*0.0.0.0/0"; then - echo -e "${GREEN}✓ Default REJECT rule exists${NC}" - return 0 - elif iptables -L "$NYM_CHAIN" | tail -1 | grep -q "REJECT"; then - echo -e "${GREEN}✓ Default REJECT rule exists at the end of chain${NC}" - return 0 - else - echo -e "${RED}✗ Default REJECT rule missing${NC}" - # Display the last 3 rules in the chain for debugging - echo -e "${YELLOW}Last 3 rules in the chain:${NC}" - iptables -L "$NYM_CHAIN" | tail -3 - return 1 - fi -} - -run_all_tests() { - local total_failures=0 - local total_tests=0 - local skip_default_reject=false - - # Parse arguments - while [[ $# -gt 0 ]]; do - case "$1" in - --skip-default-reject) - skip_default_reject=true - shift - ;; - *) - echo -e "${RED}Unknown argument: $1${NC}" - exit 1 - ;; - esac - done - - local test_functions=( - "test_forward_chain_hook" - "test_port_range_rules" - "test_critical_services" - ) - - if [ "$skip_default_reject" = false ]; then - test_functions+=("test_default_reject_rule") - fi - - echo -e "${YELLOW}Running Nym Exit Policy Verification Tests...${NC}" - - for test_func in "${test_functions[@]}"; do - ((total_tests++)) - $test_func - if [ $? -ne 0 ]; then - ((total_failures++)) - echo -e "${RED}Test $test_func FAILED${NC}" - else - echo -e "${GREEN}Test $test_func PASSED${NC}" - fi - done - - echo -e "\n${YELLOW}Test Summary:${NC}" - echo -e "Total Tests: $total_tests" - echo -e "Failures: $total_failures" - - if [ $total_failures -eq 0 ]; then - echo -e "${GREEN}All Tests Passed Successfully!${NC}" - exit 0 - else - echo -e "${RED}Some Tests Failed. Please review the iptables configuration.${NC}" - exit 1 - fi -} - -if [[ $EUID -ne 0 ]]; then - echo -e "${RED}This script must be run as root${NC}" - exit 1 -fi - -# Run the tests -run_all_tests "$@" From 21d52244cb077b5db9e5a7a11038647894aafd5f Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 18:49:53 +0100 Subject: [PATCH 059/180] sync up with new tunnel manager --- scripts/nym-node-setup/nym-node-cli.py | 28 ++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index 6a3f84540a..2df9657e3f 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -32,8 +32,6 @@ class NodeSetupCLI: self.landing_page_html = self.fetch_script("landing-page.html") self.nginx_proxy_wss_sh = self.fetch_script("nginx_proxy_wss_sh") self.tunnel_manager_sh = self.fetch_script("network_tunnel_manager.sh") - self.wg_ip_tables_manager_sh = self.fetch_script("wireguard-exit-policy-manager.sh") - self.wg_ip_tables_test_sh = self.fetch_script("exit-policy-tests.sh") self.quic_bridge_deployment_sh = self.fetch_script("quic_bridge_deployment.sh") else: self.landing_page_html = None @@ -190,9 +188,7 @@ class NodeSetupCLI: "start-node-systemd-service.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/start-node-systemd-service.sh", "nginx_proxy_wss_sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-nginx-proxy-wss.sh", "landing-page.html": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/landing-page.html", - "network_tunnel_manager.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/network_tunnel_manager.sh", - "wireguard-exit-policy-manager.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh", - "exit-policy-tests.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/exit-policy-tests.sh", + "network_tunnel_manager.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/network-tunnel-manager.sh", "quic_bridge_deployment.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/quic_bridge_deployment.sh" } @@ -297,9 +293,9 @@ class NodeSetupCLI: ans = input( "\nWireGuard is not configured.\n" "Nodes routing WireGuard can be listed as both entry and exit in the app.\n" - "Enable WireGuard support? (y/n): " + "Enable WireGuard support? (Y/n): " ).strip().lower() - val = "true" if ans in ("y", "yes") else "false" + val = "true" if ans in ("", "y", "yes") else "false" val = norm(val) os.environ["WIREGUARD"] = val @@ -382,13 +378,15 @@ class NodeSetupCLI: "Setting up Wireguard IP tables to match Nym exit policy for mixnet, stored at: https://nymtech.net/.wellknown/network-requester/exit-policy.txt" "\nThis may take a while, follow the steps below and don't kill the process..." ) - self.run_script(self.wg_ip_tables_manager_sh, args=["install"]) - self.run_script(self.wg_ip_tables_manager_sh, args=["status"]) - self.run_script(self.wg_ip_tables_test_sh) + self.run_script(self.tunnel_manager_sh, args=["exit_policy_install"]) def quic_bridge_deploy(self): """Setup QUIC bridge and configuration using external script""" - self.run_script(self.quic_bridge_deployment_sh, args=["full_bridge_setup"]) + print("\n* * * Installing and configuring QUIC bridges * * *") + answer = input("\nDo you want to install, setup and run QUIC bridge? (Y/n) ").strip().lower() + + if answer in ("", "y", "yes"): + self.run_script(self.quic_bridge_deployment_sh, args=["full_bridge_setup"]) def run_nym_node_as_service(self): """Starts /etc/systemd/system/nym-node.service based on prompt using external script""" @@ -416,8 +414,8 @@ class NodeSetupCLI: if is_active: while True: - ans = input(f"{service} is already running. Restart it now? (y/n):\n").strip().lower() - if ans == "y": + ans = input(f"{service} is already running. Restart it now? (Y/n):\n").strip().lower() + if ans in ("", "Y", "y"): self.run_script(self.start_node_systemd_service_sh, args=["restart-poll"], env=run_env) return elif ans == "n": @@ -427,8 +425,8 @@ class NodeSetupCLI: print("Invalid input. Please press 'y' or 'n' and press enter.") else: while True: - ans = input(f"{service} is not running. Start it now? (y/n):\n").strip().lower() - if ans == "y": + ans = input(f"{service} is not running. Start it now? (Y/n):\n").strip().lower() + if ans in ("", "Y", "y"): self.run_script(self.start_node_systemd_service_sh, args=["start-poll"], env=run_env) return elif ans == "n": From fe7470ea44fd7539bae5a4946a4cdc6061326fe0 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:54:17 +0000 Subject: [PATCH 060/180] address comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/quic_bridge_deployment.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/nym-node-setup/quic_bridge_deployment.sh b/scripts/nym-node-setup/quic_bridge_deployment.sh index 1d40958578..4aaa7d72df 100644 --- a/scripts/nym-node-setup/quic_bridge_deployment.sh +++ b/scripts/nym-node-setup/quic_bridge_deployment.sh @@ -32,6 +32,8 @@ log() { # simple redirection that keeps function scope intact add_log_redirection() { + # Preserve original stdout (fd 1) and stderr (fd 2) in file descriptors 3 and 4 + # before redirection, to support potential future use cases where the original streams are needed. exec 3>&1 4>&2 exec > >(tee -a "$LOG_FILE") 2>&1 } From e8ca490db121c1059dff723be62e85b73d55d841 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:54:42 +0000 Subject: [PATCH 061/180] style Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/quic_bridge_deployment.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/nym-node-setup/quic_bridge_deployment.sh b/scripts/nym-node-setup/quic_bridge_deployment.sh index 4aaa7d72df..f0a3de83df 100644 --- a/scripts/nym-node-setup/quic_bridge_deployment.sh +++ b/scripts/nym-node-setup/quic_bridge_deployment.sh @@ -433,7 +433,6 @@ apply_bridge_iptables_rules() { iptables-save > /etc/iptables/rules.v4 ip6tables-save > /etc/iptables/rules.v6 - ok "iptables rules applied." } From 94151965bbebb3559bee101d850beeb242188f89 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:55:09 +0000 Subject: [PATCH 062/180] string to dict fix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/nym-node-cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index 2df9657e3f..21eacbac48 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -133,7 +133,7 @@ class NodeSetupCLI: mode = getattr(args, "mode", None) if mode: mode = mode.strip().lower() - self._upsert_env_vars("MODE", mode) + self._upsert_env_vars({"MODE": mode}) print(f"Mode set to '{mode}' from CLI argument.") return mode From 6b8a6283a428e0677f4753889e3e166181754d3c Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 20:27:43 +0100 Subject: [PATCH 063/180] fix nginx script --- .../nym-node-setup/setup-nginx-proxy-wss.sh | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh index eae4d7165e..f3a8aa46c0 100644 --- a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh +++ b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh @@ -64,16 +64,16 @@ cert_ok() { } fetch_landing_html() { - local url="https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/landing-page.html" + local url="https://raw.githubusercontent.com/nymtech/nym/develop/scripts/nym-node-setup/landing-page.html" + mkdir -p "${WEBROOT}" - if command -v curl >/dev/null 2>&1; then - curl -fsSL "$url" -o "${WEBROOT}/index.html" || true - else - wget -qO "${WEBROOT}/index.html" "$url" || true - fi + curl -fsSL "$url" -o "${WEBROOT}/index.html" || \ + wget -qO "${WEBROOT}/index.html" "$url" || true + # fallback HTML if [[ ! -s "${WEBROOT}/index.html" ]]; then + echo "landing page not found upstream, using fallback template" cat > "${WEBROOT}/index.html" <<'HTML' @@ -158,23 +158,36 @@ echo "Landing page at ${WEBROOT}/index.html" # disable default and stale SSL configs [[ -L "${SITES_EN}/default" ]] && unlink "${SITES_EN}/default" || true + +# hard-clean: dangling SSL symlink for this hostname +if [[ -L "${SITES_EN}/${HOSTNAME}-ssl" && ! -f "${SITES_AVAIL}/${HOSTNAME}-ssl" ]]; then + echo "Removing stale SSL symlink for ${HOSTNAME}" + unlink "${SITES_EN}/${HOSTNAME}-ssl" +fi + +# remove SSL vhosts referencing localhost certs for f in "${SITES_EN}"/*; do [[ -L "$f" ]] || continue if grep -q "/etc/letsencrypt/live/localhost" "$f"; then - echo "Disabling vhost referencing localhost cert: $f"; unlink "$f" + echo "Disabling vhost with localhost cert: $f" + unlink "$f" fi done + +# remove SSL vhosts with missing certs for f in "${SITES_EN}"/*; do [[ -L "$f" ]] || continue if grep -qE 'listen\s+.*443' "$f"; then cert=$(awk '/ssl_certificate[ \t]+/ {print $2}' "$f" | tr -d ';' | head -n1) - key=$(awk '/ssl_certificate_key[ \t]+/ {print $2}' "$f" | tr -d ';' | head -n1) - if [[ -n "${cert:-}" && ! -s "$cert" ]] || [[ -n "${key:-}" && ! -s "$key" ]]; then - echo "Disabling SSL vhost with missing cert/key: $f"; unlink "$f" + key=$(awk '/ssl_certificate_key[ \t]+/ {print $2}' "$f" | tr -d ';') + if [[ ! -s "$cert" || ! -s "$key" ]]; then + echo "Disabling SSL vhost with missing cert/key: $f" + unlink "$f" fi fi done + # HTTP :80 vhost (ACME-safe, proxy to :8080) neat_backup "${BASE_HTTP}" cat > "${BASE_HTTP}" < Date: Thu, 13 Nov 2025 20:37:35 +0100 Subject: [PATCH 064/180] flush nginx script anew --- .../nym-node-setup/setup-nginx-proxy-wss.sh | 368 +++++------------- 1 file changed, 100 insertions(+), 268 deletions(-) diff --git a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh index f3a8aa46c0..ed3d3e59fb 100644 --- a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh +++ b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh @@ -1,365 +1,197 @@ #!/usr/bin/env bash set -euo pipefail -# load env (prefer absolute ENV_FILE injected by Python CLI; fallback to ./env.sh) +# load env if [[ -n "${ENV_FILE:-}" && -f "${ENV_FILE}" ]]; then set -a; . "${ENV_FILE}"; set +a elif [[ -f "./env.sh" ]]; then set -a; . ./env.sh; set +a fi -: "${HOSTNAME:?HOSTNAME not set in env.sh}" -: "${EMAIL:?EMAIL not set in env.sh}" +: "${HOSTNAME:?HOSTNAME not set}" +: "${EMAIL:?EMAIL not set}" -export SYSTEMD_PAGER="" -export SYSTEMD_COLORS="0" -DEBIAN_FRONTEND=noninteractive - -# sanity check -if [[ "${HOSTNAME}" == "localhost" || "${HOSTNAME}" == "127.0.0.1" || "${HOSTNAME}" == "ubuntu" ]]; then - echo "ERROR: HOSTNAME cannot be 'localhost'. Use a public FQDN." >&2 - exit 1 -fi - -echo -e "\n* * * Starting nginx configuration for landing page, reverse proxy and WSS * * *" - -# set paths & ports vars +export DEBIAN_FRONTEND=noninteractive WEBROOT="/var/www/${HOSTNAME}" -LE_ACME_DIR="/var/www/letsencrypt" SITES_AVAIL="/etc/nginx/sites-available" SITES_EN="/etc/nginx/sites-enabled" -BASE_HTTP="${SITES_AVAIL}/${HOSTNAME}" # :80 vhost -BASE_HTTPS="${SITES_AVAIL}/${HOSTNAME}-ssl" # :443 vhost (we’ll write it ourselves) -WSS_AVAIL="${SITES_AVAIL}/wss-config-nym" -BACKUP_DIR="/etc/nginx/sites-backups" -NYM_PORT_HTTP="${NYM_PORT_HTTP:-8080}" -NYM_PORT_WSS="${NYM_PORT_WSS:-9000}" -WSS_LISTEN_PORT="${WSS_LISTEN_PORT:-9001}" +HTTP_CONF="${SITES_AVAIL}/${HOSTNAME}" +HTTPS_CONF="${SITES_AVAIL}/${HOSTNAME}-ssl" +WSS_CONF="${SITES_AVAIL}/wss-config-nym" -mkdir -p "${WEBROOT}" "${LE_ACME_DIR}" "${BACKUP_DIR}" "${SITES_AVAIL}" "${SITES_EN}" +echo +echo "* * * starting clean nginx configuration for landing page, reverse proxy and wss * * *" -# helpers -neat_backup() { - local file="$1"; [[ -f "$file" ]] || return 0 - local sha_now; sha_now="$(sha256sum "$file" | awk '{print $1}')" || return 0 - local tag; tag="$(basename "$file")" - local latest="${BACKUP_DIR}/${tag}.latest" - if [[ -f "$latest" ]]; then - local sha_prev; sha_prev="$(awk '{print $1}' "$latest")" - [[ "$sha_now" == "$sha_prev" ]] && return 0 - fi - cp -a "$file" "${BACKUP_DIR}/${tag}.bak.$(date +%s)" - echo "$sha_now ${tag}" > "$latest" - ls -1t "${BACKUP_DIR}/${tag}.bak."* 2>/dev/null | tail -n +6 | xargs -r rm -f -} +############################################################################### +# step 1: clean old configs +############################################################################### -ensure_enabled() { - local src="$1"; local name; name="$(basename "$src")" - ln -sf "$src" "${SITES_EN}/${name}" -} +echo "cleaning existing nginx configuration" -cert_ok() { - [[ -s "/etc/letsencrypt/live/${HOSTNAME}/fullchain.pem" && -s "/etc/letsencrypt/live/${HOSTNAME}/privkey.pem" ]] -} +# remove old default symlink +[[ -L ${SITES_EN}/default ]] && unlink ${SITES_EN}/default || true -fetch_landing_html() { - local url="https://raw.githubusercontent.com/nymtech/nym/develop/scripts/nym-node-setup/landing-page.html" +# remove old vhosts for this domain (symlink + config) +rm -f "${SITES_EN}/${HOSTNAME}" || true +rm -f "${SITES_EN}/${HOSTNAME}-ssl" || true +rm -f "${SITES_EN}/wss-config-nym" || true - mkdir -p "${WEBROOT}" +rm -f "${HTTP_CONF}" || true +rm -f "${HTTPS_CONF}" || true +rm -f "${WSS_CONF}" || true - curl -fsSL "$url" -o "${WEBROOT}/index.html" || \ - wget -qO "${WEBROOT}/index.html" "$url" || true +############################################################################### +# step 2: create landing page +############################################################################### - # fallback HTML - if [[ ! -s "${WEBROOT}/index.html" ]]; then - echo "landing page not found upstream, using fallback template" - cat > "${WEBROOT}/index.html" <<'HTML' +mkdir -p "${WEBROOT}" +if ! curl -fsSL \ + https://raw.githubusercontent.com/nymtech/nym/develop/scripts/nym-node-setup/landing-page.html \ + -o "${WEBROOT}/index.html"; then + + cat > "${WEBROOT}/index.html" <<'EOF' - - - - - Nym Exit Gateway - - - -

Nym Exit Gateway

-

This is a Nym Exit Gateway. The operator of this router has no access to any of the data routing through that due to encryption design.

+ +nym node + +

nym exit gateway

+

this is a nym exit gateway.

-HTML - fi -} +EOF -inject_email() { - local file="${WEBROOT}/index.html" - [[ -n "${EMAIL:-}" && -s "$file" ]] || return 0 - - # Escape characters that would break sed replacement - local esc_email - esc_email="$(printf '%s' "$EMAIL" | sed -e 's/[&/\]/\\&/g')" - - # try to update existing meta (case-insensitive on the name attr) - if grep -qiE ']+name=["'"'"']contact:email["'"'"']' "$file"; then - sed -i -E \ - "s|(]+name=[\"']contact:email[\"'][^>]*content=\")[^\"]*(\"[^>]*>)|\1${esc_email}\2|I" \ - "$file" || true - return 0 - fi - - # insert before if present (case-insensitive) - if grep -qi '' "$file"; then - awk -v email="$EMAIL" ' - BEGIN{IGNORECASE=1} - /<\/head>/ && !done { - print " " - done=1 - } - { print } - ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" - return 0 - fi - - # fallback: append at end - printf '\n\n' "$EMAIL" >> "$file" || true -} - -fetch_logo() { - local logo_url="https://raw.githubusercontent.com/nymtech/websites/refs/heads/main/www/nym.com/public/images/Nym_meta_Image.png?token=GHSAT0AAAAAACEERII7URYRTFACZ4F2OWZ42GMCPBQ" - mkdir -p "${WEBROOT}/images" - if [[ ! -s "${WEBROOT}/images/nym_logo.png" ]]; then - if command -v curl >/dev/null 2>&1; then - curl -fsSL "$logo_url" -o "${WEBROOT}/images/nym_logo.png" || true - else - wget -qO "${WEBROOT}/images/nym_logo.png" "$logo_url" || true - fi - fi -} - -reload_nginx() { nginx -t && systemctl reload nginx; } - -# landing page (idempotent) -fetch_landing_html -inject_email -fetch_logo -echo "Landing page at ${WEBROOT}/index.html" - -# disable default and stale SSL configs -[[ -L "${SITES_EN}/default" ]] && unlink "${SITES_EN}/default" || true - -# hard-clean: dangling SSL symlink for this hostname -if [[ -L "${SITES_EN}/${HOSTNAME}-ssl" && ! -f "${SITES_AVAIL}/${HOSTNAME}-ssl" ]]; then - echo "Removing stale SSL symlink for ${HOSTNAME}" - unlink "${SITES_EN}/${HOSTNAME}-ssl" fi -# remove SSL vhosts referencing localhost certs -for f in "${SITES_EN}"/*; do - [[ -L "$f" ]] || continue - if grep -q "/etc/letsencrypt/live/localhost" "$f"; then - echo "Disabling vhost with localhost cert: $f" - unlink "$f" - fi -done +echo "landing page at ${WEBROOT}/index.html" -# remove SSL vhosts with missing certs -for f in "${SITES_EN}"/*; do - [[ -L "$f" ]] || continue - if grep -qE 'listen\s+.*443' "$f"; then - cert=$(awk '/ssl_certificate[ \t]+/ {print $2}' "$f" | tr -d ';' | head -n1) - key=$(awk '/ssl_certificate_key[ \t]+/ {print $2}' "$f" | tr -d ';') - if [[ ! -s "$cert" || ! -s "$key" ]]; then - echo "Disabling SSL vhost with missing cert/key: $f" - unlink "$f" - fi - fi -done +############################################################################### +# step 3: create basic http config (80) +############################################################################### - -# HTTP :80 vhost (ACME-safe, proxy to :8080) -neat_backup "${BASE_HTTP}" -cat > "${BASE_HTTP}" < "${HTTP_CONF}" </dev/null; then - echo "WARNING: Can't reach Let's Encrypt directory. We'll still keep HTTP up." >&2 -fi -THIS_IP="$(curl -fsS -4 https://ifconfig.me || true)" -DNS_IP="$(getent ahostsv4 "${HOSTNAME}" 2>/dev/null | awk '{print $1; exit}')" -echo "Public IPv4: ${THIS_IP:-unknown} DNS A(${HOSTNAME}): ${DNS_IP:-unresolved}" -if [[ -n "${THIS_IP:-}" && -n "${DNS_IP:-}" && "${THIS_IP}" != "${DNS_IP}" ]]; then - echo "WARNING: DNS for ${HOSTNAME} does not match this server's public IPv4." -fi -timedatectl show -p NTPSynchronized --value 2>/dev/null | grep -qi yes || timedatectl set-ntp true || true +ln -sf "${HTTP_CONF}" "${SITES_EN}/${HOSTNAME}" -# install certbot if missing -if ! command -v certbot >/dev/null 2>&1; then - if command -v snap >/dev/null 2>&1; then - snap install core || true; snap refresh core || true - snap install --classic certbot; ln -sf /snap/bin/certbot /usr/bin/certbot - else - apt-get update -y >/dev/null 2>&1 || true - apt-get install -y certbot >/dev/null 2>&1 || true - fi -fi +nginx -t +systemctl reload nginx -# issue/renew via WEBROOT (no nginx auto-edit), non-fatal if it fails -STAGING_FLAG=""; [[ "${CERTBOT_STAGING:-0}" == "1" ]] && STAGING_FLAG="--staging" && echo "Using Let's Encrypt STAGING." -if ! cert_ok; then - certbot certonly --non-interactive --agree-tos -m "${EMAIL}" -d "${HOSTNAME}" \ - --webroot -w "${LE_ACME_DIR}" ${STAGING_FLAG} || true -fi +############################################################################### +# step 4: get certificate using certbot --nginx +############################################################################### -# create own 443 vhost (only if certs exist) -if cert_ok; then - neat_backup "${BASE_HTTPS}" - cat > "${BASE_HTTPS}" </dev/null 2>&1 || true +apt-get install -y certbot python3-certbot-nginx >/dev/null 2>&1 || true + +echo "requesting let's encrypt certificate for ${HOSTNAME}" + +certbot --nginx --non-interactive --agree-tos \ + -m "${EMAIL}" -d "${HOSTNAME}" --redirect || true + +############################################################################### +# step 5: if cert OK, build https and wss configs +############################################################################### + +if [[ -s "/etc/letsencrypt/live/${HOSTNAME}/fullchain.pem" ]]; then + echo "certificate detected, creating https and wss configs" + + cat > "${HTTPS_CONF}" <HTTPS (keeps ACME path in HTTP too via separate small server) - neat_backup "${BASE_HTTP}" - cat > "${BASE_HTTP}" < "${WSS_CONF}" < "${WSS_AVAIL}" < Date: Thu, 13 Nov 2025 20:46:33 +0100 Subject: [PATCH 065/180] replace y to Y and '' --- scripts/nym-node-setup/nym-node-install.sh | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/scripts/nym-node-setup/nym-node-install.sh b/scripts/nym-node-setup/nym-node-install.sh index 3ea25687f5..3a88577ad9 100644 --- a/scripts/nym-node-setup/nym-node-install.sh +++ b/scripts/nym-node-setup/nym-node-install.sh @@ -34,8 +34,9 @@ check_existing_config() { if [[ "${resp}" =~ ^([Rr][Ee][Ss][Ee][Tt])$ ]]; then echo - read -r -p "We are going to remove the existing node with configuration $NODE_CONFIG_DIR and replace it with a fresh one, do you want to back up the old one first? (y/n) " backup_ans - if [[ "${backup_ans}" =~ ^[Yy]$ ]]; then + echo "We are going to remove the existing node with configuration $NODE_CONFIG_DIR and replace it with a fresh one." + read -r -p "back up the old one first? (Y/n) " backup_ans + if [[ -z "${backup_ans}" || "${backup_ans}" =~ ^[Yy]$ ]]; then ts="$(date +%Y%m%d-%H%M%S)" backup_dir="$HOME/.nym/backup/$(basename "$NODE_CONFIG_DIR")-$ts" echo "Backing up to: $backup_dir" @@ -185,14 +186,14 @@ NYM_NODE="$HOME/nym-binaries/nym-node" if [[ -e "${NYM_NODE}" ]]; then echo echo -e "\n* * * A nym-node binary already exists at: ${NYM_NODE}" - read -r -p "Overwrite with the latest release? (y/n): " ow_ans - if [[ "${ow_ans}" =~ ^[Yy]$ ]]; then - echo "Removing existing binary to avoid 'text file busy'..." + read -r -p "Overwrite with the latest release? (Y/n): " ow_ans + if [[ -z "${ow_ans}" || "${ow_ans}" =~ ^[Yy]$ ]]; then + echo "Removing existing binary..." rm -f "${NYM_NODE}" + download_nym_node "$LATEST_TAG_URL" "$NYM_NODE" else - echo "Keeping existing binary." + echo "Keeping existing binary. Skipping download." fi -fi download_nym_node "$LATEST_TAG_URL" "$NYM_NODE" @@ -229,13 +230,12 @@ if [[ ( "$MODE" == "entry-gateway" || "$MODE" == "exit-gateway" ) && ( -n "${ASK # show current default in the prompt if present def_hint="" [[ -n "${WIREGUARD}" ]] && def_hint=" [current: ${WIREGUARD}]" - read -r -p "Enable WireGuard support? (y/n)${def_hint}: " answer || true - case "${answer:-}" in - [Yy]* ) WIREGUARD="true" ;; - [Nn]* ) WIREGUARD="false" ;; - * ) : ;; # keep existing value if user just pressed enter - esac -fi + read -r -p "Enable WireGuard support? (Y/n)${def_hint}: " answer || true + if [[ -z "${answer}" || "${answer}" =~ ^[Yy]$ ]]; then + WIREGUARD="true" + elif [[ "${answer}" =~ ^[Nn]$ ]]; then + WIREGUARD="false" + fi # final default only if still empty WIREGUARD="${WIREGUARD:-false}" From 58c0e289c25d67ba72a868629e7624a38b680087 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 13 Nov 2025 20:56:04 +0100 Subject: [PATCH 066/180] syntax fix --- scripts/nym-node-setup/nym-node-install.sh | 15 ++-- .../nym-node-setup/setup-nginx-proxy-wss.sh | 70 +++++++++++-------- 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/scripts/nym-node-setup/nym-node-install.sh b/scripts/nym-node-setup/nym-node-install.sh index 3a88577ad9..af36c92017 100644 --- a/scripts/nym-node-setup/nym-node-install.sh +++ b/scripts/nym-node-setup/nym-node-install.sh @@ -182,11 +182,12 @@ fi NYM_NODE="$HOME/nym-binaries/nym-node" -# if binary already exists, ask to overwrite; if yes, remove first +# if binary already exists, ask to overwrite; if yes, remove first; if no, skip download if [[ -e "${NYM_NODE}" ]]; then echo echo -e "\n* * * A nym-node binary already exists at: ${NYM_NODE}" read -r -p "Overwrite with the latest release? (Y/n): " ow_ans + if [[ -z "${ow_ans}" || "${ow_ans}" =~ ^[Yy]$ ]]; then echo "Removing existing binary..." rm -f "${NYM_NODE}" @@ -195,11 +196,13 @@ if [[ -e "${NYM_NODE}" ]]; then echo "Keeping existing binary. Skipping download." fi -download_nym_node "$LATEST_TAG_URL" "$NYM_NODE" +else + # binary does not exist → must download + download_nym_node "$LATEST_TAG_URL" "$NYM_NODE" +fi echo -e "\n * * * Making binary executable * * *" chmod +x "${NYM_NODE}" - echo "---------------------------------------------------" echo "Nym node binary downloaded:" "${NYM_NODE}" --version || true @@ -226,16 +229,18 @@ WIREGUARD="${WIREGUARD:-}" if [[ ( "$MODE" == "entry-gateway" || "$MODE" == "exit-gateway" ) && ( -n "${ASK_WG:-}" || -z "$WIREGUARD" ) ]]; then echo echo "Gateways can also route WireGuard in NymVPN." - echo "Enabling it means your node may be listed as both entry and exit in the app." - # show current default in the prompt if present def_hint="" [[ -n "${WIREGUARD}" ]] && def_hint=" [current: ${WIREGUARD}]" + read -r -p "Enable WireGuard support? (Y/n)${def_hint}: " answer || true + if [[ -z "${answer}" || "${answer}" =~ ^[Yy]$ ]]; then WIREGUARD="true" elif [[ "${answer}" =~ ^[Nn]$ ]]; then WIREGUARD="false" fi +fi + # final default only if still empty WIREGUARD="${WIREGUARD:-false}" diff --git a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh index ed3d3e59fb..312ea8bf11 100644 --- a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh +++ b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh @@ -12,6 +12,7 @@ fi : "${EMAIL:?EMAIL not set}" export DEBIAN_FRONTEND=noninteractive + WEBROOT="/var/www/${HOSTNAME}" SITES_AVAIL="/etc/nginx/sites-available" SITES_EN="/etc/nginx/sites-enabled" @@ -23,29 +24,31 @@ WSS_CONF="${SITES_AVAIL}/wss-config-nym" echo echo "* * * starting clean nginx configuration for landing page, reverse proxy and wss * * *" -############################################################################### -# step 1: clean old configs -############################################################################### +################################################################################ +# step 1: clean all previous configs +################################################################################ echo "cleaning existing nginx configuration" -# remove old default symlink -[[ -L ${SITES_EN}/default ]] && unlink ${SITES_EN}/default || true +# remove default nginx config +[[ -L "${SITES_EN}/default" ]] && unlink "${SITES_EN}/default" || true -# remove old vhosts for this domain (symlink + config) -rm -f "${SITES_EN}/${HOSTNAME}" || true -rm -f "${SITES_EN}/${HOSTNAME}-ssl" || true -rm -f "${SITES_EN}/wss-config-nym" || true +# remove domain symlinks +rm -f "${SITES_EN}/${HOSTNAME}" || true +rm -f "${SITES_EN}/${HOSTNAME}-ssl" || true +rm -f "${SITES_EN}/wss-config-nym" || true -rm -f "${HTTP_CONF}" || true -rm -f "${HTTPS_CONF}" || true -rm -f "${WSS_CONF}" || true +# remove old configs +rm -f "${HTTP_CONF}" || true +rm -f "${HTTPS_CONF}" || true +rm -f "${WSS_CONF}" || true -############################################################################### -# step 2: create landing page -############################################################################### +################################################################################ +# step 2: landing page +################################################################################ mkdir -p "${WEBROOT}" + if ! curl -fsSL \ https://raw.githubusercontent.com/nymtech/nym/develop/scripts/nym-node-setup/landing-page.html \ -o "${WEBROOT}/index.html"; then @@ -65,9 +68,9 @@ fi echo "landing page at ${WEBROOT}/index.html" -############################################################################### -# step 3: create basic http config (80) -############################################################################### +################################################################################ +# step 3: HTTP :80 config +################################################################################ cat > "${HTTP_CONF}" </dev/null 2>&1 || true apt-get install -y certbot python3-certbot-nginx >/dev/null 2>&1 || true @@ -108,15 +113,17 @@ apt-get install -y certbot python3-certbot-nginx >/dev/null 2>&1 || true echo "requesting let's encrypt certificate for ${HOSTNAME}" certbot --nginx --non-interactive --agree-tos \ + --reuse-key \ -m "${EMAIL}" -d "${HOSTNAME}" --redirect || true -############################################################################### -# step 5: if cert OK, build https and wss configs -############################################################################### +################################################################################ +# step 5: HTTPS and WSS configs +################################################################################ if [[ -s "/etc/letsencrypt/live/${HOSTNAME}/fullchain.pem" ]]; then echo "certificate detected, creating https and wss configs" + # HTTPS 443 cat > "${HTTPS_CONF}" < "${WSS_CONF}" < Date: Fri, 14 Nov 2025 10:31:26 +0000 Subject: [PATCH 067/180] remove redundant detect interface Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../nym-node-setup/network-tunnel-manager.sh | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 66e412d629..400a75971f 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -37,21 +37,25 @@ fi TUNNEL_INTERFACE="${TUNNEL_INTERFACE:-nymtun0}" WG_INTERFACE="${WG_INTERFACE:-nymwg}" +# Function to detect and validate uplink interface +detect_uplink_interface() { + local cmd="$1" + local dev + dev="$(eval "$cmd" 2>/dev/null | awk '{print \$5}' | head -n1 || true)" + if [[ -n "$dev" && "$dev" =~ ^[a-zA-Z0-9._-]+$ ]]; then + echo "$dev" + else + echo "" + fi +} + # uplink device detection, can be overridden NETWORK_DEVICE="${NETWORK_DEVICE:-}" if [[ -z "$NETWORK_DEVICE" ]]; then - NETWORK_DEVICE="$(ip -o route show default 2>/dev/null | awk '{print $5}' | head -n1 || true)" - # Validate that NETWORK_DEVICE is non-empty and looks like a network interface - if [[ -z "$NETWORK_DEVICE" || ! "$NETWORK_DEVICE" =~ ^[a-zA-Z0-9._-]+$ ]]; then - NETWORK_DEVICE="" - fi + NETWORK_DEVICE="$(detect_uplink_interface "ip -o route show default")" fi if [[ -z "$NETWORK_DEVICE" ]]; then - NETWORK_DEVICE="$(ip -o route show default table all 2>/dev/null | awk '{print $5}' | head -n1 || true)" - # Validate that NETWORK_DEVICE is non-empty and looks like a network interface - if [[ -z "$NETWORK_DEVICE" || ! "$NETWORK_DEVICE" =~ ^[a-zA-Z0-9._-]+$ ]]; then - NETWORK_DEVICE="" - fi + NETWORK_DEVICE="$(detect_uplink_interface "ip -o route show default table all")" fi if [[ -z "$NETWORK_DEVICE" ]]; then echo "cannot determine uplink interface. set NETWORK_DEVICE or UPLINK_DEV" From f62dbbdae0b81b8e920501164fd61e34c6230a6d Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:33:23 +0000 Subject: [PATCH 068/180] ensure idempotency for the iptable rules Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../nym-node-setup/network-tunnel-manager.sh | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 400a75971f..668b6c55f1 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -521,14 +521,28 @@ setup_nat_rules() { configure_exit_dns_and_icmp() { echo "ensuring dns and icmp are allowed inside nym exit chain" - iptables -I "$NYM_CHAIN" 1 -p udp --dport 53 -j ACCEPT - iptables -I "$NYM_CHAIN" 2 -p tcp --dport 53 -j ACCEPT - ip6tables -I "$NYM_CHAIN" 1 -p udp --dport 53 -j ACCEPT - ip6tables -I "$NYM_CHAIN" 2 -p tcp --dport 53 -j ACCEPT + if ! iptables -C "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT 2>/dev/null; then + iptables -A "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT + fi + if ! iptables -C "$NYM_CHAIN" -p tcp --dport 53 -j ACCEPT 2>/dev/null; then + iptables -A "$NYM_CHAIN" -p tcp --dport 53 -j ACCEPT + fi + if ! ip6tables -C "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT 2>/dev/null; then + ip6tables -A "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT + fi + if ! ip6tables -C "$NYM_CHAIN" -p tcp --dport 53 -j ACCEPT 2>/dev/null; then + ip6tables -A "$NYM_CHAIN" -p tcp --dport 53 -j ACCEPT + fi - iptables -I "$NYM_CHAIN" 3 -p icmp --icmp-type echo-request -j ACCEPT - iptables -I "$NYM_CHAIN" 4 -p icmp --icmp-type echo-reply -j ACCEPT - ip6tables -I "$NYM_CHAIN" 3 -p ipv6-icmp -j ACCEPT + if ! iptables -C "$NYM_CHAIN" -p icmp --icmp-type echo-request -j ACCEPT 2>/dev/null; then + iptables -A "$NYM_CHAIN" -p icmp --icmp-type echo-request -j ACCEPT + fi + if ! iptables -C "$NYM_CHAIN" -p icmp --icmp-type echo-reply -j ACCEPT 2>/dev/null; then + iptables -A "$NYM_CHAIN" -p icmp --icmp-type echo-reply -j ACCEPT + fi + if ! ip6tables -C "$NYM_CHAIN" -p ipv6-icmp -j ACCEPT 2>/dev/null; then + ip6tables -A "$NYM_CHAIN" -p ipv6-icmp -j ACCEPT + fi } apply_port_allowlist() { From 3f560180b7713428979732fb705151340131aaed Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:35:42 +0000 Subject: [PATCH 069/180] remove redundant Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/quic_bridge_deployment.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/nym-node-setup/quic_bridge_deployment.sh b/scripts/nym-node-setup/quic_bridge_deployment.sh index f0a3de83df..d6fb15cb2e 100644 --- a/scripts/nym-node-setup/quic_bridge_deployment.sh +++ b/scripts/nym-node-setup/quic_bridge_deployment.sh @@ -32,9 +32,6 @@ log() { # simple redirection that keeps function scope intact add_log_redirection() { - # Preserve original stdout (fd 1) and stderr (fd 2) in file descriptors 3 and 4 - # before redirection, to support potential future use cases where the original streams are needed. - exec 3>&1 4>&2 exec > >(tee -a "$LOG_FILE") 2>&1 } add_log_redirection From 054715a600d503e823dd76324e0ecbde8b19b439 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:36:23 +0000 Subject: [PATCH 070/180] robust error handling Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/nym-node-cli.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index 21eacbac48..b6e4bd55a1 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -96,8 +96,12 @@ class NodeSetupCLI: if ip.returncode == 0 and ip.stdout.strip(): updated["PUBLIC_IP"] = ip.stdout.strip() os.environ["PUBLIC_IP"] = ip.stdout.strip() - except Exception: - pass + except subprocess.TimeoutExpired: + print("[WARN] Timeout expired while trying to fetch public IP with curl.") + except FileNotFoundError: + print("[WARN] 'curl' command not found. Please install curl or set PUBLIC_IP manually.") + except subprocess.CalledProcessError as e: + print(f"[WARN] Error while running curl to fetch public IP: {e}") # write all collected variables to env.sh in one go self._upsert_env_vars(updated, env_file) From d820131d2cc539d9eb14bcad966154a7c9b2ecf0 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:36:45 +0000 Subject: [PATCH 071/180] arg consistency Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/nym-node-setup/nym-node-cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index b6e4bd55a1..0e9130c1bc 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -585,6 +585,7 @@ class NodeSetupCLI: # Pass uplink override to all helper scripts if provided if getattr(args, "uplink_dev", None): os.environ["UPLINK_DEV"] = args.uplink_dev + os.environ["NETWORK_DEVICE"] = args.uplink_dev self.run_script(self.prereqs_install_sh) self.run_script(self.node_install_sh) self.run_script(self.service_config_sh) From 228ef8b158b7772106062ce30d6a6c87bbb36b29 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Fri, 14 Nov 2025 12:40:14 +0100 Subject: [PATCH 072/180] add else --- scripts/nym-node-setup/nym-node-cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index 0e9130c1bc..179ab0e767 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -391,6 +391,8 @@ class NodeSetupCLI: if answer in ("", "y", "yes"): self.run_script(self.quic_bridge_deployment_sh, args=["full_bridge_setup"]) + else: + print("Skipping QUIC bridge setup.") def run_nym_node_as_service(self): """Starts /etc/systemd/system/nym-node.service based on prompt using external script""" From 9bdd2af14c24bd05ae152718cbb2b01f4fa02570 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Fri, 14 Nov 2025 12:42:05 +0100 Subject: [PATCH 073/180] enforce root --- scripts/nym-node-setup/nym-node-prereqs-install.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/nym-node-setup/nym-node-prereqs-install.sh b/scripts/nym-node-setup/nym-node-prereqs-install.sh index 58b04ecc65..f359aceb67 100644 --- a/scripts/nym-node-setup/nym-node-prereqs-install.sh +++ b/scripts/nym-node-setup/nym-node-prereqs-install.sh @@ -1,6 +1,11 @@ #!/bin/bash -# update, upgrade & install dependencies +if [[ "$(id -u)" -ne 0 ]]; then + echo "This script must be run as root." + exit 1 +fi + +# update, upgrade and install dependencies echo -e "\n* * * Installing needed prerequisities * * *" apt update -y && apt --fix-broken install @@ -8,11 +13,9 @@ apt upgrade apt install apt ca-certificates jq curl wget ufw jq tmux pkg-config build-essential libssl-dev git ntp ntpdate neovim tree tmux tig nginx -y apt install ufw --fix-missing - - # enable & setup firewall echo -e "\n* * * Setting up firewall using ufw * * * " -echo "Please enable the firewall in the next prompt for node proper routing!" +echo "Please enable the firewall in the next prompt for node proper routing." echo ufw enable ufw allow 22/tcp # SSH - you're in control of these ports From 10707fd2c5be2c354e2340c436c6014c022b7a0a Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Fri, 14 Nov 2025 12:43:54 +0100 Subject: [PATCH 074/180] convention Y/n --- scripts/nym-node-setup/setup-systemd-service-file.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/nym-node-setup/setup-systemd-service-file.sh b/scripts/nym-node-setup/setup-systemd-service-file.sh index 4f8d586492..fca48f7a2f 100644 --- a/scripts/nym-node-setup/setup-systemd-service-file.sh +++ b/scripts/nym-node-setup/setup-systemd-service-file.sh @@ -83,8 +83,8 @@ fi # interactive path (manual runs) ensure_mode -read -rp "Service file not found. Create it now? (y/n): " create_ans -if [[ "${create_ans:-}" =~ ^[Yy]$ ]]; then +read -rp "Service file not found. Create it now? (Y/n): " create_ans +if [[ -z "${create_ans}" || "${create_ans}" =~ ^[Yy]$ ]]; then create_service_file else echo "Not creating the service file." From e0ff09f323c1fdb8b9873872cb11a932eae171fa Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Fri, 14 Nov 2025 12:44:57 +0100 Subject: [PATCH 075/180] enforce root --- scripts/nym-node-setup/setup-nginx-proxy-wss.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh index 312ea8bf11..7acb56f120 100644 --- a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh +++ b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh @@ -1,6 +1,11 @@ #!/usr/bin/env bash set -euo pipefail +if [[ "$(id -u)" -ne 0 ]]; then + echo "This script must be run as root." + exit 1 +fi + # load env if [[ -n "${ENV_FILE:-}" && -f "${ENV_FILE}" ]]; then set -a; . "${ENV_FILE}"; set +a From ae47d53f0ce2d9dd7f766884eeb9f2ac8ce9c1d2 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Fri, 14 Nov 2025 12:49:10 +0100 Subject: [PATCH 076/180] enforce root --- scripts/nym-node-setup/setup-nginx-proxy-wss.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh index 7acb56f120..aa74b9a775 100644 --- a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh +++ b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh @@ -27,7 +27,7 @@ HTTPS_CONF="${SITES_AVAIL}/${HOSTNAME}-ssl" WSS_CONF="${SITES_AVAIL}/wss-config-nym" echo -echo "* * * starting clean nginx configuration for landing page, reverse proxy and wss * * *" +echo "* * * Starting clean nginx configuration for landing page, reverse proxy and wss * * *" ################################################################################ # step 1: clean all previous configs From cc04a09ed7463d9362aea30a15a11c0a253b5806 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Fri, 14 Nov 2025 12:58:03 +0100 Subject: [PATCH 077/180] remove redundant work --- .../nym-node-setup/quic_bridge_deployment.sh | 87 ++++++++++++------- 1 file changed, 54 insertions(+), 33 deletions(-) diff --git a/scripts/nym-node-setup/quic_bridge_deployment.sh b/scripts/nym-node-setup/quic_bridge_deployment.sh index d6fb15cb2e..2d6b151feb 100644 --- a/scripts/nym-node-setup/quic_bridge_deployment.sh +++ b/scripts/nym-node-setup/quic_bridge_deployment.sh @@ -56,7 +56,7 @@ if [[ -z "$NET_DEV" ]]; then echo -e "${RED}Cannot determine uplink interface. Set UPLINK_DEV.${RESET}" | tee -a "$LOG_FILE" exit 1 fi -info "Using uplink device: $NET_DEV" +echo "Using uplink device: $NET_DEV" WG_IFACE="nymwg" @@ -191,13 +191,31 @@ verify_bridge_prerequisites() { } adjust_ip_forwarding() { - title "Adjusting IP Forwarding" - sed -i '/^net\.ipv4\.ip_forward=/d' /etc/sysctl.conf - sed -i '/^net\.ipv6\.conf\.all\.forwarding=/d' /etc/sysctl.conf - echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf - echo "net.ipv6.conf.all.forwarding=1" >> /etc/sysctl.conf - sysctl -p /etc/sysctl.conf - ok "IPv4/IPv6 forwarding enabled." + title "Checking IP forwarding" + local v4 v6 + v4="$(cat /proc/sys/net/ipv4/ip_forward 2>/dev/null || echo 0)" + v6="$(cat /proc/sys/net/ipv6/conf/all/forwarding 2>/dev/null || echo 0)" + + if [[ "$v4" == "1" ]]; then + ok "IPv4 forwarding is enabled." + else + warn "IPv4 forwarding is not enabled." + fi + + if [[ "$v6" == "1" ]]; then + ok "IPv6 forwarding is enabled." + else + warn "IPv6 forwarding is not enabled." + fi + + if [[ "$v4" != "1" || "$v6" != "1" ]]; then + echo + echo "To enable forwarding and routing consistently, run the network tunnel manager script as root." + echo "For example:" + echo " ./network-tunnel-manager.sh complete_networking_configuration" + echo "or:" + echo " ./network-tunnel-manager.sh adjust_ip_forwarding" + fi } # Install nym-bridge @@ -410,37 +428,40 @@ EOF # IPTABLES & helpers apply_bridge_iptables_rules() { - title "Applying iptables rules" + title "Checking iptables rules for bridge routing" - # Ensure stateful rules exist - iptables -C FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ - iptables -I FORWARD 1 -m state --state RELATED,ESTABLISHED -j ACCEPT - ip6tables -C FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ - ip6tables -I FORWARD 1 -m state --state RELATED,ESTABLISHED -j ACCEPT + echo "Inspecting current iptables state for interface ${WG_IFACE} and uplink ${NET_DEV}." + echo - # Allow WG interface input - iptables -C INPUT -i "$WG_IFACE" -j ACCEPT 2>/dev/null || iptables -I INPUT -i "$WG_IFACE" -j ACCEPT - ip6tables -C INPUT -i "$WG_IFACE" -j ACCEPT 2>/dev/null || ip6tables -I INPUT -i "$WG_IFACE" -j ACCEPT + echo "IPv4 FORWARD:" + iptables -L FORWARD -n -v 2>/dev/null | sed -n '1,20p' || echo "iptables not available." + echo + echo "IPv4 NAT POSTROUTING:" + iptables -t nat -L POSTROUTING -n -v 2>/dev/null | sed -n '1,20p' || true + echo + echo "IPv6 FORWARD:" + ip6tables -L FORWARD -n -v 2>/dev/null | sed -n '1,20p' || true + echo + echo "IPv6 NAT POSTROUTING:" + ip6tables -t nat -L POSTROUTING -n -v 2>/dev/null | sed -n '1,20p' || true - # NAT (idempotent) - iptables -t nat -C POSTROUTING -o "$NET_DEV" -j MASQUERADE 2>/dev/null || \ - iptables -t nat -I POSTROUTING 1 -o "$NET_DEV" -j MASQUERADE - ip6tables -t nat -C POSTROUTING -o "$NET_DEV" -j MASQUERADE 2>/dev/null || \ - ip6tables -t nat -I POSTROUTING 1 -o "$NET_DEV" -j MASQUERADE - - iptables-save > /etc/iptables/rules.v4 - ip6tables-save > /etc/iptables/rules.v6 - ok "iptables rules applied." + echo + echo "This script no longer changes iptables. To configure routing and NAT for nym, use the network tunnel manager script." + echo "For example (run as root):" + echo " ./network-tunnel-manager.sh complete_networking_configuration" } configure_dns_and_icmp() { - title "Allow ICMP and DNS" - iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT || true - ip6tables -A INPUT -p ipv6-icmp -j ACCEPT || true - iptables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null || iptables -I INPUT -p udp --dport 53 -j ACCEPT - ip6tables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null || ip6tables -I INPUT -p udp --dport 53 -j ACCEPT + title "Checking ICMP and DNS firewall rules" - ok "ICMP and DNS rules applied." + echo "IPv4 INPUT rules related to ICMP and DNS:" + iptables -L INPUT -n -v 2>/dev/null | grep -E 'icmp|dpt:53' || echo "no matching IPv4 rules shown." + echo + echo "IPv6 INPUT rules related to ICMP and DNS:" + ip6tables -L INPUT -n -v 2>/dev/null | grep -E 'icmp|dpt:53' || echo "no matching IPv6 rules shown." + + echo + echo "If ping or DNS are blocked for bridge traffic, adjust your firewall using the network tunnel manager script or your chosen firewall tool." } # Full interactive setup @@ -485,7 +506,7 @@ full_bridge_setup() { press_enter "Press Enter to continue..." echo "" - echo "Step 6/6: Configuring network rules (optional but recommended)..." + echo "Step 6/6: Checking network rules and forwarding status..." adjust_ip_forwarding apply_bridge_iptables_rules configure_dns_and_icmp From cc95358385ba60fe7137b0b954cd03228b1fbc5a Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:08:05 +0100 Subject: [PATCH 078/180] add email to a fallback --- scripts/nym-node-setup/setup-nginx-proxy-wss.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh index aa74b9a775..4b0fae6bda 100644 --- a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh +++ b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh @@ -65,6 +65,7 @@ if ! curl -fsSL \

nym exit gateway

this is a nym exit gateway.

+

Operator contact: ${EMAIL}

EOF From ce261059860ff560e7958092624374e0026f354c Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Fri, 14 Nov 2025 13:24:05 +0100 Subject: [PATCH 079/180] typo --- .../docs/pages/operators/nodes/nym-node/configuration.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx index 69afc1e41d..dbe24c5072 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx @@ -375,7 +375,7 @@ operation check_nymtun_iptables completed successfully. ###### 5. Remove old and apply new rules for wireguad routing ```sh -/network_tunnel_manager.sh remove_duplicate_rules nymwg +./network_tunnel_manager.sh remove_duplicate_rules nymwg ./network_tunnel_manager.sh apply_iptables_rules_wg ``` From 842ce93a60a075ee246783d02b965820e3876a54 Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Fri, 14 Nov 2025 13:24:20 +0100 Subject: [PATCH 080/180] remove duplicate ufw rule --- scripts/nym-node-setup/nym-node-prereqs-install.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/nym-node-setup/nym-node-prereqs-install.sh b/scripts/nym-node-setup/nym-node-prereqs-install.sh index f359aceb67..e462ecb5c9 100644 --- a/scripts/nym-node-setup/nym-node-prereqs-install.sh +++ b/scripts/nym-node-setup/nym-node-prereqs-install.sh @@ -28,6 +28,5 @@ ufw allow 9000/tcp # Nym Specific - clients port ufw allow 9001/tcp # Nym specific - wss port ufw allow 51822/udp # WireGuard ufw allow in on nymwg to any port 51830 proto tcp # bandwidth queries/topup - inside the tunnel -ufw allow 'Nginx Full' && \ ufw reload && \ ufw status From 6b2bb3029b2b3356bd48a4fb988ff2c8df4cb068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 14 Nov 2025 13:13:15 +0000 Subject: [PATCH 081/180] feat: merge intermediate upgrade mode changes (#6174) * squashing feat: merge intermediate upgrade mode changes #6174 to more easily resolve merge conflicts during rebasing added additional v2 query for metadata endpoint for requesting upgrade mode recheck added additional message to v6 authenticator to request explicit upgrade mode recheck clippy test fixes due to updated keys updated assertion for upgrading v1 top up request to v2 compare attester public key against the expected value within the credential proxy use pre-generated attestation public keys within nym-nodes remove version deprecation bugfix: default bandwidth response for authenticator expose upgrade mode information in authenticator responses adding tests for new v2 server passing upgrade mode information in metadata endpoint v2 wireguard private metadata bugfix: make sure to immediately poll for attestation after spawning task fix gateway probe and remove code duplication for finalizing registration squashing before rebasing post rebasing fixes AuthenticatorVersion helpers additional nits allow unwraps in mocks fixed linux build clippy integrating upgrade mode into authenticator fixed build after adding wrappers to response types conditionally updating peer handle bandwidth cleanup negotiate initial protocol during registration change auth to use highest protocol handler for JWT message dont meter client bandwidth in upgrade mode handling recheck requests sending information about upgrade_mode on client messages gateway watching for upgrade mode attestation wip: gateways to disable bandwidth metering on upgrade mode * fixed ServerResponse deserialisation * fixed incorrect swagger path for upgrade mode check endpoint * moved upgrade mode endpoint out of bandwidth routes * chore: remove unused error variant * removed re-export of UpgradeModeAttestation from credentials-interface * chore: define single source of truth for minimum bandwidth threshold value * moved type definitions out of traits.rs * updated v6 versioning to point to niolo release instead * fixed incorrect error mapping --- Cargo.lock | 30 +- Cargo.toml | 1 + common/authenticator-requests/Cargo.toml | 7 + .../src/client_message.rs | 476 +++++++++------- common/authenticator-requests/src/error.rs | 11 + common/authenticator-requests/src/lib.rs | 4 +- common/authenticator-requests/src/models.rs | 52 ++ common/authenticator-requests/src/request.rs | 53 +- common/authenticator-requests/src/response.rs | 51 +- common/authenticator-requests/src/traits.rs | 490 +++++++++++++++-- .../src/v1/registration.rs | 2 +- .../authenticator-requests/src/v1/response.rs | 6 +- .../src/v2/conversion.rs | 4 +- .../src/v2/registration.rs | 2 +- .../authenticator-requests/src/v2/response.rs | 6 +- .../src/v3/conversion.rs | 16 +- .../src/v3/registration.rs | 2 +- .../authenticator-requests/src/v3/response.rs | 6 +- .../src/v4/conversion.rs | 16 +- .../src/v4/registration.rs | 2 +- .../authenticator-requests/src/v4/response.rs | 6 +- .../src/v5/conversion.rs | 8 +- .../src/v5/registration.rs | 2 +- .../authenticator-requests/src/v5/response.rs | 6 +- .../src/v6/conversion.rs | 441 +++++++++++++++ common/authenticator-requests/src/v6/mod.rs | 15 + .../src/v6/registration.rs | 287 ++++++++++ .../authenticator-requests/src/v6/request.rs | 135 +++++ .../authenticator-requests/src/v6/response.rs | 153 ++++++ common/authenticator-requests/src/v6/topup.rs | 15 + .../src/v6/upgrade_mode_check.rs | 12 + common/authenticator-requests/src/version.rs | 26 +- common/client-libs/gateway-client/Cargo.toml | 3 + .../gateway-client/src/bandwidth.rs | 78 ++- .../gateway-client/src/client/config.rs | 4 +- .../gateway-client/src/client/mod.rs | 212 +++++--- .../client-libs/gateway-client/src/error.rs | 3 + .../gateway-client/src/packet_router.rs | 2 + .../gateway-client/src/socket_state.rs | 25 +- common/credential-proxy/src/error.rs | 13 +- common/credential-verification/Cargo.toml | 3 +- .../src/bandwidth_storage_manager.rs | 8 +- common/credential-verification/src/error.rs | 19 + common/credential-verification/src/lib.rs | 1 + .../src/upgrade_mode.rs | 284 ++++++++++ common/credentials-interface/Cargo.toml | 2 + common/credentials-interface/src/lib.rs | 29 + common/gateway-requests/Cargo.toml | 3 + common/gateway-requests/src/lib.rs | 66 ++- .../src/registration/handshake/client.rs | 26 +- .../src/registration/handshake/error.rs | 8 + .../src/registration/handshake/gateway.rs | 43 +- .../src/registration/handshake/messages.rs | 11 +- .../src/registration/handshake/mod.rs | 224 ++++++-- .../src/registration/handshake/state.rs | 119 ++-- .../gateway-requests/src/shared_key/legacy.rs | 5 + common/gateway-requests/src/shared_key/mod.rs | 2 + common/gateway-requests/src/types/error.rs | 2 +- .../types/registration_handshake_wrapper.rs | 21 +- .../src/types/text_request/authenticate.rs | 8 +- .../src/types/text_request/mod.rs | 31 +- .../src/types/text_response.rs | 126 ++++- .../src/lib.rs | 11 + common/upgrade-mode-check/src/error.rs | 4 +- common/upgrade-mode-check/src/jwt.rs | 10 +- common/upgrade-mode-check/src/lib.rs | 5 +- .../client/src/lib.rs | 17 + .../server/src/http/router.rs | 9 +- .../server/src/http/state.rs | 124 ++++- .../server/src/network.rs | 64 ++- .../server/src/transceiver.rs | 4 +- .../shared/src/conversion_helpers.rs | 218 ++++++++ .../shared/src/error.rs | 3 + .../shared/src/lib.rs | 3 +- .../shared/src/models/error.rs | 2 +- .../shared/src/models/interface.rs | 186 +++++-- .../shared/src/models/mod.rs | 22 +- .../models/v0/available_bandwidth/request.rs | 71 +-- .../models/v0/available_bandwidth/response.rs | 70 +-- .../shared/src/models/v0/interface.rs | 46 +- .../shared/src/models/v0/mod.rs | 78 +-- .../src/models/v0/topup_bandwidth/request.rs | 72 +-- .../src/models/v0/topup_bandwidth/response.rs | 72 +-- .../models/v1/available_bandwidth/request.rs | 70 +-- .../models/v1/available_bandwidth/response.rs | 70 +-- .../shared/src/models/v1/interface.rs | 78 ++- .../shared/src/models/v1/mod.rs | 88 +-- .../src/models/v1/topup_bandwidth/request.rs | 72 +-- .../src/models/v1/topup_bandwidth/response.rs | 72 +-- .../src/models/v2/available_bandwidth/mod.rs | 5 + .../models/v2/available_bandwidth/request.rs | 50 ++ .../models/v2/available_bandwidth/response.rs | 56 ++ .../src/models/v2/check_upgrade_mode/mod.rs | 5 + .../models/v2/check_upgrade_mode/request.rs | 76 +++ .../models/v2/check_upgrade_mode/response.rs | 52 ++ .../shared/src/models/v2/interface.rs | 304 +++++++++++ .../shared/src/models/v2/mod.rs | 197 +++++++ .../src/models/v2/topup_bandwidth/mod.rs | 5 + .../src/models/v2/topup_bandwidth/request.rs | 61 +++ .../src/models/v2/topup_bandwidth/response.rs | 56 ++ .../shared/src/routes.rs | 2 + .../tests/Cargo.toml | 7 +- .../tests/src/lib.rs | 478 +++++++++++++--- .../tests/src/mock_connect_info.rs | 121 +++++ .../tests/src/v0/app_state.rs | 7 + .../tests/src/v0/interface.rs | 150 ------ .../tests/src/v0/mod.rs | 3 +- .../tests/src/v0/network.rs | 95 ++-- .../tests/src/v0/peer_controller.rs | 9 + .../tests/src/v1/app_state.rs | 33 ++ .../tests/src/v1/mod.rs | 6 + .../tests/src/v1/network.rs | 134 +++++ .../tests/src/v1/peer_controller.rs | 9 + .../tests/src/v2/app_state.rs | 8 + .../tests/src/v2/mod.rs | 6 + .../tests/src/v2/network.rs | 329 +++++++++++ .../tests/src/v2/peer_controller.rs | 177 ++++++ common/wireguard-types/Cargo.toml | 5 +- common/wireguard-types/src/public_key.rs | 13 + common/wireguard/Cargo.toml | 15 +- common/wireguard/src/lib.rs | 12 +- common/wireguard/src/peer_controller.rs | 42 +- common/wireguard/src/peer_handle.rs | 123 ++--- common/wireguard/src/peer_storage_manager.rs | 45 +- envs/canary.env | 2 + envs/mainnet.env | 2 + envs/sandbox.env | 2 + gateway/Cargo.toml | 13 +- gateway/src/config.rs | 29 + .../node/client_handling/active_clients.rs | 9 +- .../client_handling/websocket/common_state.rs | 4 +- .../connection_handler/authenticated.rs | 91 +++- .../websocket/connection_handler/fresh.rs | 144 +++-- .../websocket/connection_handler/mod.rs | 5 +- .../authenticator/error.rs | 22 +- .../authenticator/mixnet_listener.rs | 509 +++++++++++------- .../authenticator/mod.rs | 13 +- .../authenticator/peer_manager.rs | 14 +- gateway/src/node/mod.rs | 71 ++- gateway/src/node/upgrade_mode/mod.rs | 4 + gateway/src/node/upgrade_mode/watcher.rs | 146 +++++ nym-authenticator-client/src/error.rs | 5 +- nym-authenticator-client/src/lib.rs | 285 +++++----- nym-authenticator-client/src/types.rs | 16 + .../nym-credential-proxy-requests/src/lib.rs | 2 - .../src/attestation_watcher.rs | 13 +- .../nym-credential-proxy/src/cli.rs | 4 + .../nym-credential-proxy/src/helpers.rs | 24 + .../src/http/state/nyx_upgrade_mode.rs | 16 +- nym-gateway-probe/src/lib.rs | 66 +-- nym-node/src/cli/commands/run/args.rs | 8 +- nym-node/src/cli/helpers.rs | 18 +- nym-node/src/config/gateway_tasks.rs | 157 +++++- nym-node/src/config/helpers.rs | 13 + nym-node/src/config/mod.rs | 15 +- .../src/config/old_configs/old_config_v10.rs | 12 +- nym-node/src/config/template.rs | 12 + nym-node/src/env.rs | 2 + nym-node/src/node/mod.rs | 29 +- nym-wallet/Cargo.lock | 264 ++++++++- 160 files changed, 7878 insertions(+), 2124 deletions(-) create mode 100644 common/authenticator-requests/src/models.rs create mode 100644 common/authenticator-requests/src/v6/conversion.rs create mode 100644 common/authenticator-requests/src/v6/mod.rs create mode 100644 common/authenticator-requests/src/v6/registration.rs create mode 100644 common/authenticator-requests/src/v6/request.rs create mode 100644 common/authenticator-requests/src/v6/response.rs create mode 100644 common/authenticator-requests/src/v6/topup.rs create mode 100644 common/authenticator-requests/src/v6/upgrade_mode_check.rs create mode 100644 common/credential-verification/src/upgrade_mode.rs create mode 100644 common/wireguard-private-metadata/shared/src/conversion_helpers.rs create mode 100644 common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/mod.rs create mode 100644 common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/request.rs create mode 100644 common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/response.rs create mode 100644 common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/mod.rs create mode 100644 common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/request.rs create mode 100644 common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/response.rs create mode 100644 common/wireguard-private-metadata/shared/src/models/v2/interface.rs create mode 100644 common/wireguard-private-metadata/shared/src/models/v2/mod.rs create mode 100644 common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/mod.rs create mode 100644 common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/request.rs create mode 100644 common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/response.rs create mode 100644 common/wireguard-private-metadata/tests/src/mock_connect_info.rs create mode 100644 common/wireguard-private-metadata/tests/src/v0/app_state.rs delete mode 100644 common/wireguard-private-metadata/tests/src/v0/interface.rs create mode 100644 common/wireguard-private-metadata/tests/src/v0/peer_controller.rs create mode 100644 common/wireguard-private-metadata/tests/src/v1/app_state.rs create mode 100644 common/wireguard-private-metadata/tests/src/v1/mod.rs create mode 100644 common/wireguard-private-metadata/tests/src/v1/network.rs create mode 100644 common/wireguard-private-metadata/tests/src/v1/peer_controller.rs create mode 100644 common/wireguard-private-metadata/tests/src/v2/app_state.rs create mode 100644 common/wireguard-private-metadata/tests/src/v2/mod.rs create mode 100644 common/wireguard-private-metadata/tests/src/v2/network.rs create mode 100644 common/wireguard-private-metadata/tests/src/v2/peer_controller.rs create mode 100644 gateway/src/node/upgrade_mode/mod.rs create mode 100644 gateway/src/node/upgrade_mode/watcher.rs create mode 100644 nym-authenticator-client/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index 6ff6f8bb68..b9bcc7eb58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4988,6 +4988,7 @@ dependencies = [ "nym-network-defaults", "nym-service-provider-requests-common", "nym-sphinx", + "nym-test-utils", "nym-wireguard-types", "rand 0.8.5", "semver 1.0.26", @@ -4995,6 +4996,7 @@ dependencies = [ "sha2 0.10.9", "strum_macros", "thiserror 2.0.12", + "tracing", "x25519-dalek", ] @@ -5605,12 +5607,13 @@ dependencies = [ "nym-api-requests", "nym-credentials", "nym-credentials-interface", + "nym-crypto", "nym-ecash-contract-common", "nym-gateway-requests", "nym-gateway-storage", "nym-task", + "nym-upgrade-mode-check", "nym-validator-client", - "rand 0.8.5", "si-scale", "thiserror 2.0.12", "time", @@ -5650,6 +5653,7 @@ dependencies = [ "nym-compact-ecash", "nym-ecash-time", "nym-network-defaults", + "nym-upgrade-mode-check", "rand 0.8.5", "serde", "strum", @@ -5800,7 +5804,6 @@ dependencies = [ name = "nym-gateway" version = "1.1.36" dependencies = [ - "anyhow", "async-trait", "bincode", "bip39", @@ -5811,7 +5814,6 @@ dependencies = [ "futures", "ipnetwork", "mock_instant", - "nym-api-requests", "nym-authenticator-requests", "nym-client-core", "nym-credential-verification", @@ -5824,7 +5826,6 @@ dependencies = [ "nym-id", "nym-ip-packet-router", "nym-mixnet-client", - "nym-mixnode-common", "nym-network-defaults", "nym-network-requester", "nym-node-metrics", @@ -5834,20 +5835,18 @@ dependencies = [ "nym-statistics-common", "nym-task", "nym-topology", - "nym-types", + "nym-upgrade-mode-check", "nym-validator-client", "nym-wireguard", "nym-wireguard-private-metadata-server", "nym-wireguard-types", "rand 0.8.5", "serde", - "sha2 0.10.9", "thiserror 2.0.12", "time", "tokio", "tokio-stream", "tokio-tungstenite", - "tokio-util", "tracing", "url", "zeroize", @@ -7574,17 +7573,10 @@ dependencies = [ name = "nym-wireguard" version = "0.1.0" dependencies = [ - "async-trait", "base64 0.22.1", - "bincode", - "chrono", - "dashmap", "defguard_wireguard_rs", - "dyn-clone", "futures", "ip_network", - "log", - "nym-authenticator-requests", "nym-credential-verification", "nym-credentials-interface", "nym-crypto", @@ -7595,11 +7587,9 @@ dependencies = [ "nym-task", "nym-wireguard-types", "thiserror 2.0.12", - "time", "tokio", "tokio-stream", "tracing", - "x25519-dalek", ] [[package]] @@ -7651,15 +7641,20 @@ version = "1.0.0" dependencies = [ "async-trait", "axum", + "futures", "nym-credential-verification", "nym-credentials-interface", + "nym-crypto", "nym-http-api-client", "nym-http-api-common", + "nym-upgrade-mode-check", "nym-wireguard", "nym-wireguard-private-metadata-client", "nym-wireguard-private-metadata-server", "nym-wireguard-private-metadata-shared", + "time", "tokio", + "tower 0.5.2", "tower-http 0.5.2", "utoipa", ] @@ -7669,10 +7664,7 @@ name = "nym-wireguard-types" version = "0.1.0" dependencies = [ "base64 0.22.1", - "log", - "nym-config", "nym-crypto", - "nym-network-defaults", "rand 0.8.5", "serde", "thiserror 2.0.12", diff --git a/Cargo.toml b/Cargo.toml index c07db1f4ba..92f5fdbb7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -171,6 +171,7 @@ members = [ default-members = [ "clients/native", "clients/socks5", + "nym-authenticator-client", "nym-api", "nym-credential-proxy/nym-credential-proxy", "nym-node", diff --git a/common/authenticator-requests/Cargo.toml b/common/authenticator-requests/Cargo.toml index 60ff6826ba..6126a18f80 100644 --- a/common/authenticator-requests/Cargo.toml +++ b/common/authenticator-requests/Cargo.toml @@ -16,6 +16,7 @@ serde = { workspace = true, features = ["derive"] } semver = { workspace = true } strum_macros = { workspace = true } thiserror = { workspace = true } +tracing = { workspace = true } nym-credentials-interface = { path = "../credentials-interface" } nym-crypto = { path = "../crypto", features = ["asymmetric"] } @@ -29,7 +30,13 @@ hmac = { workspace = true, optional = true } sha2 = { workspace = true, optional = true } x25519-dalek = { workspace = true, features = ["static_secrets"] } +[dev-dependencies] +nym-test-utils = { path = "../test-utils" } + [features] default = ["verify"] # this is moved to a separate feature as we really need clients to import it (especially, *cough*, wasm) verify = ["hmac", "sha2"] + +[lints] +workspace = true \ No newline at end of file diff --git a/common/authenticator-requests/src/client_message.rs b/common/authenticator-requests/src/client_message.rs index 06a910b9b9..23bdd13ab0 100644 --- a/common/authenticator-requests/src/client_message.rs +++ b/common/authenticator-requests/src/client_message.rs @@ -6,9 +6,8 @@ use nym_wireguard_types::PeerPublicKey; use crate::{ AuthenticatorVersion, Error, - latest::registration::IpPair, traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, Versionable}, - v2, v3, v4, v5, + v2, v3, v4, v5, v6, }; // This is very redundant with AuthenticatorRequest and I reckon they could be smooshed. @@ -21,6 +20,272 @@ pub enum ClientMessage { TopUp(Box), } +pub struct SerialisedRequest { + pub bytes: Vec, + pub request_id: u64, +} + +impl SerialisedRequest { + pub fn new(bytes: Vec, request_id: u64) -> Self { + Self { bytes, request_id } + } +} + +impl ClientMessage { + fn serialise_v1(&self) -> Result { + Err(Error::UnsupportedVersion) + } + + fn serialise_v2(&self, reply_to: Recipient) -> Result { + use v2::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, + request::AuthenticatorRequest, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request( + InitMessage { + pub_key: init_message.pub_key(), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request( + FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ip: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)? + .into(), + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message + .credential() + .and_then(|c| c.credential.into_zk_nym()) + .map(|c| *c), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = + AuthenticatorRequest::new_query_request(query_message.pub_key(), reply_to); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + _ => Err(Error::UnsupportedMessage), + } + } + + fn serialise_v3(&self, reply_to: Recipient) -> Result { + use v3::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, + request::AuthenticatorRequest, + topup::TopUpMessage, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request( + InitMessage { + pub_key: init_message.pub_key(), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request( + FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ip: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)? + .into(), + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message + .credential() + .and_then(|c| c.credential.into_zk_nym()) + .map(|c| *c), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = + AuthenticatorRequest::new_query_request(query_message.pub_key(), reply_to); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::TopUp(top_up_message) => { + let (req, id) = AuthenticatorRequest::new_topup_request( + TopUpMessage { + pub_key: top_up_message.pub_key(), + credential: top_up_message.credential(), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + } + } + + fn serialise_v4(&self, reply_to: Recipient) -> Result { + use v4::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair}, + request::AuthenticatorRequest, + topup::TopUpMessage, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request( + InitMessage { + pub_key: init_message.pub_key(), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request( + FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ips: IpPair { + ipv4: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)?, + ipv6: final_message + .gateway_client_ipv6() + .ok_or(Error::UnsupportedMessage)?, + }, + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message + .credential() + .and_then(|c| c.credential.into_zk_nym()) + .map(|c| *c), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = + AuthenticatorRequest::new_query_request(query_message.pub_key(), reply_to); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::TopUp(top_up_message) => { + let (req, id) = AuthenticatorRequest::new_topup_request( + TopUpMessage { + pub_key: top_up_message.pub_key(), + credential: top_up_message.credential(), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + } + } + + fn serialise_v5(&self) -> Result { + use v5::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair}, + request::AuthenticatorRequest, + topup::TopUpMessage, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage { + pub_key: init_message.pub_key(), + }); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ips: IpPair { + ipv4: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)?, + ipv6: final_message + .gateway_client_ipv6() + .ok_or(Error::UnsupportedMessage)?, + }, + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message + .credential() + .and_then(|c| c.credential.into_zk_nym()) + .map(|c| *c), + }); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = AuthenticatorRequest::new_query_request(query_message.pub_key()); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::TopUp(top_up_message) => { + let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage { + pub_key: top_up_message.pub_key(), + credential: top_up_message.credential(), + }); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + } + } + + fn serialise_v6(&self) -> Result { + use v6::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair}, + request::AuthenticatorRequest, + topup::TopUpMessage, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage { + pub_key: init_message.pub_key(), + }); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ips: IpPair { + ipv4: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)?, + ipv6: final_message + .gateway_client_ipv6() + .ok_or(Error::UnsupportedMessage)?, + }, + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message.credential(), + }); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = AuthenticatorRequest::new_query_request(query_message.pub_key()); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::TopUp(top_up_message) => { + let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage { + pub_key: top_up_message.pub_key(), + credential: top_up_message.credential(), + }); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + } + } +} + impl ClientMessage { // check if message is wasteful e.g. contains a credential pub fn is_wasteful(&self) -> bool { @@ -40,205 +305,14 @@ impl ClientMessage { } } - pub fn bytes(&self, reply_to: Recipient) -> Result<(Vec, u64), Error> { + pub fn bytes(&self, reply_to: Recipient) -> Result { match self.version() { - AuthenticatorVersion::V1 => Err(Error::UnsupportedVersion), - AuthenticatorVersion::V2 => { - use v2::{ - registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, - request::AuthenticatorRequest, - }; - match self { - ClientMessage::Initial(init_message) => { - let (req, id) = AuthenticatorRequest::new_initial_request( - InitMessage { - pub_key: init_message.pub_key(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Final(final_message) => { - let (req, id) = AuthenticatorRequest::new_final_request( - FinalMessage { - gateway_client: GatewayClient { - pub_key: final_message.gateway_client_pub_key(), - private_ip: final_message - .gateway_client_ipv4() - .ok_or(Error::UnsupportedMessage)? - .into(), - mac: ClientMac::new(final_message.gateway_client_mac()), - }, - credential: final_message.credential(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Query(query_message) => { - let (req, id) = AuthenticatorRequest::new_query_request( - query_message.pub_key(), - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - _ => Err(Error::UnsupportedMessage), - } - } - AuthenticatorVersion::V3 => { - use v3::{ - registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, - request::AuthenticatorRequest, - topup::TopUpMessage, - }; - match self { - ClientMessage::Initial(init_message) => { - let (req, id) = AuthenticatorRequest::new_initial_request( - InitMessage { - pub_key: init_message.pub_key(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Final(final_message) => { - let (req, id) = AuthenticatorRequest::new_final_request( - FinalMessage { - gateway_client: GatewayClient { - pub_key: final_message.gateway_client_pub_key(), - private_ip: final_message - .gateway_client_ipv4() - .ok_or(Error::UnsupportedMessage)? - .into(), - mac: ClientMac::new(final_message.gateway_client_mac()), - }, - credential: final_message.credential(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Query(query_message) => { - let (req, id) = AuthenticatorRequest::new_query_request( - query_message.pub_key(), - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::TopUp(top_up_message) => { - let (req, id) = AuthenticatorRequest::new_topup_request( - TopUpMessage { - pub_key: top_up_message.pub_key(), - credential: top_up_message.credential(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - } - } - AuthenticatorVersion::V4 => { - use v4::{ - registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, - request::AuthenticatorRequest, - topup::TopUpMessage, - }; - match self { - ClientMessage::Initial(init_message) => { - let (req, id) = AuthenticatorRequest::new_initial_request( - InitMessage { - pub_key: init_message.pub_key(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Final(final_message) => { - let (req, id) = AuthenticatorRequest::new_final_request( - FinalMessage { - gateway_client: GatewayClient { - pub_key: final_message.gateway_client_pub_key(), - private_ips: IpPair { - ipv4: final_message - .gateway_client_ipv4() - .ok_or(Error::UnsupportedMessage)?, - ipv6: final_message - .gateway_client_ipv6() - .ok_or(Error::UnsupportedMessage)?, - } - .into(), - mac: ClientMac::new(final_message.gateway_client_mac()), - }, - credential: final_message.credential(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Query(query_message) => { - let (req, id) = AuthenticatorRequest::new_query_request( - query_message.pub_key(), - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::TopUp(top_up_message) => { - let (req, id) = AuthenticatorRequest::new_topup_request( - TopUpMessage { - pub_key: top_up_message.pub_key(), - credential: top_up_message.credential(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - } - } - AuthenticatorVersion::V5 => { - use v5::{ - registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, - request::AuthenticatorRequest, - topup::TopUpMessage, - }; - match self { - ClientMessage::Initial(init_message) => { - let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage { - pub_key: init_message.pub_key(), - }); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Final(final_message) => { - let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage { - gateway_client: GatewayClient { - pub_key: final_message.gateway_client_pub_key(), - private_ips: IpPair { - ipv4: final_message - .gateway_client_ipv4() - .ok_or(Error::UnsupportedMessage)?, - ipv6: final_message - .gateway_client_ipv6() - .ok_or(Error::UnsupportedMessage)?, - }, - mac: ClientMac::new(final_message.gateway_client_mac()), - }, - credential: final_message.credential(), - }); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Query(query_message) => { - let (req, id) = - AuthenticatorRequest::new_query_request(query_message.pub_key()); - Ok((req.to_bytes()?, id)) - } - ClientMessage::TopUp(top_up_message) => { - let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage { - pub_key: top_up_message.pub_key(), - credential: top_up_message.credential(), - }); - Ok((req.to_bytes()?, id)) - } - } - } + AuthenticatorVersion::V1 => self.serialise_v1(), + AuthenticatorVersion::V2 => self.serialise_v2(reply_to), + AuthenticatorVersion::V3 => self.serialise_v3(reply_to), + AuthenticatorVersion::V4 => self.serialise_v4(reply_to), + AuthenticatorVersion::V5 => self.serialise_v5(), + AuthenticatorVersion::V6 => self.serialise_v6(), AuthenticatorVersion::UNKNOWN => Err(Error::UnknownVersion), } } @@ -247,7 +321,7 @@ impl ClientMessage { use AuthenticatorVersion::*; match self.version() { V1 | V2 | V3 | V4 => false, - V5 => true, + V5 | V6 => true, UNKNOWN => true, } } diff --git a/common/authenticator-requests/src/error.rs b/common/authenticator-requests/src/error.rs index d940bd538d..cbf0cde523 100644 --- a/common/authenticator-requests/src/error.rs +++ b/common/authenticator-requests/src/error.rs @@ -1,6 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::fmt::Display; use thiserror::Error; #[derive(Debug, Error)] @@ -37,3 +38,13 @@ pub enum Error { #[error(transparent)] Bincode(#[from] bincode::Error), } + +impl Error { + pub fn conversion(msg: impl Into) -> Self { + Error::Conversion(msg.into()) + } + + pub fn conversion_display(msg: impl Display) -> Self { + Error::Conversion(msg.to_string()) + } +} diff --git a/common/authenticator-requests/src/lib.rs b/common/authenticator-requests/src/lib.rs index b1b0159da3..226c78adee 100644 --- a/common/authenticator-requests/src/lib.rs +++ b/common/authenticator-requests/src/lib.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 pub mod client_message; +pub mod models; pub mod request; pub mod response; pub mod traits; @@ -10,13 +11,14 @@ pub mod v2; pub mod v3; pub mod v4; pub mod v5; +pub mod v6; mod error; mod util; mod version; pub use error::Error; -pub use v5 as latest; +pub use v6 as latest; pub use version::AuthenticatorVersion; pub const CURRENT_VERSION: u8 = latest::VERSION; diff --git a/common/authenticator-requests/src/models.rs b/common/authenticator-requests/src/models.rs new file mode 100644 index 0000000000..dc870df4d5 --- /dev/null +++ b/common/authenticator-requests/src/models.rs @@ -0,0 +1,52 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::{ + BandwidthCredential, CredentialSpendingData, TicketType, UnknownTicketType, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] +pub enum CurrentUpgradeModeStatus { + Enabled, + Disabled, + // everything pre-v6 + Unknown, +} + +impl From for CurrentUpgradeModeStatus { + fn from(value: bool) -> Self { + if value { + CurrentUpgradeModeStatus::Enabled + } else { + CurrentUpgradeModeStatus::Disabled + } + } +} + +impl From for Option { + fn from(value: CurrentUpgradeModeStatus) -> Self { + match value { + CurrentUpgradeModeStatus::Enabled => Some(true), + CurrentUpgradeModeStatus::Disabled => Some(false), + CurrentUpgradeModeStatus::Unknown => None, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct BandwidthClaim { + pub credential: BandwidthCredential, + pub kind: TicketType, +} + +impl TryFrom for BandwidthClaim { + type Error = UnknownTicketType; + + fn try_from(credential: CredentialSpendingData) -> Result { + Ok(BandwidthClaim { + kind: TicketType::try_from_encoded(credential.payment.t_type)?, + credential: BandwidthCredential::from(credential), + }) + } +} diff --git a/common/authenticator-requests/src/request.rs b/common/authenticator-requests/src/request.rs index 3d98a8ed5e..c1585b9bb6 100644 --- a/common/authenticator-requests/src/request.rs +++ b/common/authenticator-requests/src/request.rs @@ -4,8 +4,10 @@ use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use nym_sphinx::addressing::Recipient; -use crate::traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage}; -use crate::{v1, v2, v3, v4, v5}; +use crate::traits::{ + FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, UpgradeModeMessage, +}; +use crate::{v1, v2, v3, v4, v5, v6}; #[derive(Debug)] pub enum AuthenticatorRequest { @@ -33,6 +35,11 @@ pub enum AuthenticatorRequest { reply_to: Option, request_id: u64, }, + CheckUpgradeMode { + msg: Box, + protocol: Protocol, + request_id: u64, + }, } impl From for AuthenticatorRequest { @@ -202,3 +209,45 @@ impl From for AuthenticatorRequest { } } } + +impl From for AuthenticatorRequest { + fn from(value: v6::request::AuthenticatorRequest) -> Self { + match value.data { + v6::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + }, + v6::request::AuthenticatorRequestData::Final(final_message) => Self::Final { + msg: final_message, + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + }, + v6::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + } + } + v6::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { + Self::TopUpBandwidth { + msg: top_up_message, + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + } + } + v6::request::AuthenticatorRequestData::CheckUpgradeMode(upgrade_mode_check_msg) => { + Self::CheckUpgradeMode { + msg: Box::new(upgrade_mode_check_msg), + protocol: value.protocol, + request_id: value.request_id, + } + } + } + } +} diff --git a/common/authenticator-requests/src/response.rs b/common/authenticator-requests/src/response.rs index 8e196a7b72..84b6ae8526 100644 --- a/common/authenticator-requests/src/response.rs +++ b/common/authenticator-requests/src/response.rs @@ -1,11 +1,12 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::models::CurrentUpgradeModeStatus; use crate::traits::{ Id, PendingRegistrationResponse, RegisteredResponse, RemainingBandwidthResponse, - TopUpBandwidthResponse, + TopUpBandwidthResponse, UpgradeModeStatus, }; -use crate::{v2, v3, v4, v5}; +use crate::{v2, v3, v4, v5, v6}; #[derive(Debug)] pub enum AuthenticatorResponse { @@ -13,6 +14,29 @@ pub enum AuthenticatorResponse { Registered(Box), RemainingBandwidth(Box), TopUpBandwidth(Box), + UpgradeMode(Box), +} + +impl UpgradeModeStatus for AuthenticatorResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + match self { + AuthenticatorResponse::PendingRegistration(pending_registration_response) => { + pending_registration_response.upgrade_mode_status() + } + AuthenticatorResponse::Registered(registered_response) => { + registered_response.upgrade_mode_status() + } + AuthenticatorResponse::RemainingBandwidth(remaining_bandwidth_response) => { + remaining_bandwidth_response.upgrade_mode_status() + } + AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => { + top_up_bandwidth_response.upgrade_mode_status() + } + AuthenticatorResponse::UpgradeMode(upgrade_mode_response) => { + upgrade_mode_response.upgrade_mode_status() + } + } + } } impl Id for AuthenticatorResponse { @@ -28,6 +52,7 @@ impl Id for AuthenticatorResponse { AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => { top_up_bandwidth_response.id() } + AuthenticatorResponse::UpgradeMode(upgrade_mode_response) => upgrade_mode_response.id(), } } } @@ -104,3 +129,25 @@ impl From for AuthenticatorResponse { } } } + +impl From for AuthenticatorResponse { + fn from(value: v6::response::AuthenticatorResponse) -> Self { + match value.data { + v6::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => Self::PendingRegistration(Box::new(pending_registration_response)), + v6::response::AuthenticatorResponseData::Registered(registered_response) => { + Self::Registered(Box::new(registered_response)) + } + v6::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), + v6::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => { + Self::TopUpBandwidth(Box::new(top_up_bandwidth_response)) + } + v6::response::AuthenticatorResponseData::UpgradeMode(upgrade_mode_check_response) => { + Self::UpgradeMode(Box::new(upgrade_mode_check_response)) + } + } + } +} diff --git a/common/authenticator-requests/src/traits.rs b/common/authenticator-requests/src/traits.rs index 36e999383d..e16c0d96ba 100644 --- a/common/authenticator-requests/src/traits.rs +++ b/common/authenticator-requests/src/traits.rs @@ -1,15 +1,15 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::latest::registration::IpPair; +use crate::models::{BandwidthClaim, CurrentUpgradeModeStatus}; +use crate::{AuthenticatorVersion, Error, v1, v2, v3, v4, v5, v6}; +use nym_credentials_interface::CredentialSpendingData; +use nym_crypto::asymmetric::x25519; +use nym_wireguard_types::PeerPublicKey; use std::fmt; use std::net::{Ipv4Addr, Ipv6Addr}; - -use nym_credentials_interface::CredentialSpendingData; -use nym_crypto::asymmetric::x25519::PrivateKey; -use nym_wireguard_types::PeerPublicKey; - -use crate::latest::registration::IpPair; -use crate::{AuthenticatorVersion, Error, v1, v2, v3, v4, v5}; +use tracing::error; pub trait Versionable { fn version(&self) -> AuthenticatorVersion; @@ -51,6 +51,12 @@ impl Versionable for v5::registration::InitMessage { } } +impl Versionable for v6::registration::InitMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V6 + } +} + impl Versionable for v2::registration::FinalMessage { fn version(&self) -> AuthenticatorVersion { AuthenticatorVersion::V2 @@ -75,6 +81,12 @@ impl Versionable for v5::registration::FinalMessage { } } +impl Versionable for v6::registration::FinalMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V6 + } +} + impl Versionable for PeerPublicKey { fn version(&self) -> AuthenticatorVersion { AuthenticatorVersion::V3 @@ -98,6 +110,158 @@ impl Versionable for v5::topup::TopUpMessage { AuthenticatorVersion::V5 } } +impl Versionable for v6::topup::TopUpMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V6 + } +} + +impl Versionable for v6::upgrade_mode_check::UpgradeModeCheckRequest { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V6 + } +} + +pub trait UpgradeModeStatus: Id + fmt::Debug { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus; +} + +impl UpgradeModeStatus for v1::response::PendingRegistrationResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v1::response::RegisteredResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v1::response::RemainingBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v2::response::PendingRegistrationResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v2::response::RegisteredResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v2::response::RemainingBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} +impl UpgradeModeStatus for v3::response::PendingRegistrationResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v3::response::RegisteredResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v3::response::RemainingBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v3::response::TopUpBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v4::response::PendingRegistrationResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v4::response::RegisteredResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v4::response::RemainingBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v4::response::TopUpBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v5::response::PendingRegistrationResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v5::response::RegisteredResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v5::response::RemainingBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v5::response::TopUpBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v6::response::PendingRegistrationResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + self.upgrade_mode_enabled.into() + } +} + +impl UpgradeModeStatus for v6::response::RegisteredResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + self.upgrade_mode_enabled.into() + } +} + +impl UpgradeModeStatus for v6::response::RemainingBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + self.upgrade_mode_enabled.into() + } +} + +impl UpgradeModeStatus for v6::response::TopUpBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + self.upgrade_mode_enabled.into() + } +} + +impl UpgradeModeStatus for v6::response::UpgradeModeResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + self.upgrade_mode_enabled.into() + } +} pub trait InitMessage: Versionable + fmt::Debug { fn pub_key(&self) -> PeerPublicKey; @@ -133,14 +297,20 @@ impl InitMessage for v5::registration::InitMessage { } } +impl InitMessage for v6::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + pub trait FinalMessage: Versionable + fmt::Debug { fn gateway_client_pub_key(&self) -> PeerPublicKey; - fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error>; + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error>; fn private_ips(&self) -> IpPair; fn gateway_client_ipv4(&self) -> Option; fn gateway_client_ipv6(&self) -> Option; fn gateway_client_mac(&self) -> Vec; - fn credential(&self) -> Option; + fn credential(&self) -> Option; } impl FinalMessage for v1::GatewayClient { @@ -148,7 +318,7 @@ impl FinalMessage for v1::GatewayClient { self.pub_key } - fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> { self.verify(private_key, nonce) } @@ -171,7 +341,7 @@ impl FinalMessage for v1::GatewayClient { self.mac.to_vec() } - fn credential(&self) -> Option { + fn credential(&self) -> Option { None } } @@ -181,7 +351,7 @@ impl FinalMessage for v2::registration::FinalMessage { self.gateway_client.pub_key } - fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> { self.gateway_client.verify(private_key, nonce) } @@ -204,8 +374,12 @@ impl FinalMessage for v2::registration::FinalMessage { self.gateway_client.mac.to_vec() } - fn credential(&self) -> Option { - self.credential.clone() + fn credential(&self) -> Option { + self.credential.clone().and_then(|c| { + c.try_into() + .inspect_err(|err| error!("credential conversion error: {err}")) + .ok() + }) } } @@ -214,7 +388,7 @@ impl FinalMessage for v3::registration::FinalMessage { self.gateway_client.pub_key } - fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> { self.gateway_client.verify(private_key, nonce) } @@ -237,8 +411,12 @@ impl FinalMessage for v3::registration::FinalMessage { self.gateway_client.mac.to_vec() } - fn credential(&self) -> Option { - self.credential.clone() + fn credential(&self) -> Option { + self.credential.clone().and_then(|c| { + c.try_into() + .inspect_err(|err| error!("credential conversion error: {err}")) + .ok() + }) } } @@ -247,7 +425,42 @@ impl FinalMessage for v4::registration::FinalMessage { self.gateway_client.pub_key } - fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> { + self.gateway_client.verify(private_key, nonce) + } + + fn private_ips(&self) -> IpPair { + // v4 -> v5 -> v6 + v5::registration::IpPair::from(self.gateway_client.private_ips).into() + } + + fn gateway_client_ipv4(&self) -> Option { + Some(self.gateway_client.private_ips.ipv4) + } + + fn gateway_client_ipv6(&self) -> Option { + Some(self.gateway_client.private_ips.ipv6) + } + + fn gateway_client_mac(&self) -> Vec { + self.gateway_client.mac.to_vec() + } + + fn credential(&self) -> Option { + self.credential.clone().and_then(|c| { + c.try_into() + .inspect_err(|err| error!("credential conversion error: {err}")) + .ok() + }) + } +} + +impl FinalMessage for v5::registration::FinalMessage { + fn gateway_client_pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> { self.gateway_client.verify(private_key, nonce) } @@ -267,17 +480,21 @@ impl FinalMessage for v4::registration::FinalMessage { self.gateway_client.mac.to_vec() } - fn credential(&self) -> Option { - self.credential.clone() + fn credential(&self) -> Option { + self.credential.clone().and_then(|c| { + c.try_into() + .inspect_err(|err| error!("credential conversion error: {err}")) + .ok() + }) } } -impl FinalMessage for v5::registration::FinalMessage { +impl FinalMessage for v6::registration::FinalMessage { fn gateway_client_pub_key(&self) -> PeerPublicKey { self.gateway_client.pub_key } - fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> { self.gateway_client.verify(private_key, nonce) } @@ -297,7 +514,7 @@ impl FinalMessage for v5::registration::FinalMessage { self.gateway_client.mac.to_vec() } - fn credential(&self) -> Option { + fn credential(&self) -> Option { self.credential.clone() } } @@ -347,10 +564,42 @@ impl TopUpMessage for v5::topup::TopUpMessage { } } +impl TopUpMessage for v6::topup::TopUpMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn credential(&self) -> CredentialSpendingData { + self.credential.clone() + } +} + +pub trait UpgradeModeMessage: Versionable + fmt::Debug { + // the idea is to expose different types of emergency credentials here, + // like upgrade mode JWT, emergency threshold credential issued by signers, etc. + fn upgrade_mode_global_attestation_jwt(&self) -> Option; +} + +impl UpgradeModeMessage for v6::upgrade_mode_check::UpgradeModeCheckRequest { + fn upgrade_mode_global_attestation_jwt(&self) -> Option { + use v6::upgrade_mode_check::UpgradeModeCheckRequest; + + match self { + UpgradeModeCheckRequest::UpgradeModeJwt { token } => Some(token.clone()), + } + } +} + pub trait Id { fn id(&self) -> u64; } +impl Id for v1::response::PendingRegistrationResponse { + fn id(&self) -> u64 { + self.request_id + } +} + impl Id for v2::response::PendingRegistrationResponse { fn id(&self) -> u64 { self.request_id @@ -375,6 +624,18 @@ impl Id for v5::response::PendingRegistrationResponse { } } +impl Id for v6::response::PendingRegistrationResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v1::response::RegisteredResponse { + fn id(&self) -> u64 { + self.request_id + } +} + impl Id for v2::response::RegisteredResponse { fn id(&self) -> u64 { self.request_id @@ -399,6 +660,18 @@ impl Id for v5::response::RegisteredResponse { } } +impl Id for v6::response::RegisteredResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v1::response::RemainingBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + impl Id for v2::response::RemainingBandwidthResponse { fn id(&self) -> u64 { self.request_id @@ -423,6 +696,12 @@ impl Id for v5::response::RemainingBandwidthResponse { } } +impl Id for v6::response::RemainingBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + impl Id for v3::response::TopUpBandwidthResponse { fn id(&self) -> u64 { self.request_id @@ -441,11 +720,28 @@ impl Id for v5::response::TopUpBandwidthResponse { } } -pub trait PendingRegistrationResponse: Id + fmt::Debug { +impl Id for v6::response::TopUpBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v6::response::UpgradeModeResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +pub trait PendingRegistrationResponse: Id + UpgradeModeStatus + fmt::Debug { fn nonce(&self) -> u64; - fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error>; + fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error>; fn pub_key(&self) -> PeerPublicKey; fn private_ips(&self) -> IpPair; + fn finalise_registration( + &self, + private_key: &x25519::PrivateKey, + credential: Option, + ) -> Box; } impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse { @@ -453,7 +749,7 @@ impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse { self.reply.nonce } - fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> { + fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> { self.reply.gateway_data.verify(gateway_key, self.nonce()) } @@ -464,6 +760,22 @@ impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse { fn private_ips(&self) -> IpPair { self.reply.gateway_data.private_ip.into() } + + fn finalise_registration( + &self, + private_key: &x25519::PrivateKey, + credential: Option, + ) -> Box { + Box::new(v2::registration::FinalMessage { + gateway_client: v2::registration::GatewayClient::new( + private_key, + self.pub_key().inner(), + self.private_ips().ipv4.into(), + self.nonce(), + ), + credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)), + }) + } } impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse { @@ -471,7 +783,7 @@ impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse { self.reply.nonce } - fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> { + fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> { self.reply.gateway_data.verify(gateway_key, self.nonce()) } @@ -482,6 +794,22 @@ impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse { fn private_ips(&self) -> IpPair { self.reply.gateway_data.private_ip.into() } + + fn finalise_registration( + &self, + private_key: &x25519::PrivateKey, + credential: Option, + ) -> Box { + Box::new(v3::registration::FinalMessage { + gateway_client: v3::registration::GatewayClient::new( + private_key, + self.pub_key().inner(), + self.private_ips().ipv4.into(), + self.nonce(), + ), + credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)), + }) + } } impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse { @@ -489,7 +817,42 @@ impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse { self.reply.nonce } - fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> { + fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> { + self.reply.gateway_data.verify(gateway_key, self.nonce()) + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.gateway_data.pub_key + } + + fn private_ips(&self) -> IpPair { + // v4 -> v5 -> v6 + v5::registration::IpPair::from(self.reply.gateway_data.private_ips).into() + } + + fn finalise_registration( + &self, + private_key: &x25519::PrivateKey, + credential: Option, + ) -> Box { + Box::new(v4::registration::FinalMessage { + gateway_client: v4::registration::GatewayClient::new( + private_key, + self.pub_key().inner(), + self.reply.gateway_data.private_ips, + self.nonce(), + ), + credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)), + }) + } +} + +impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse { + fn nonce(&self) -> u64 { + self.reply.nonce + } + + fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> { self.reply.gateway_data.verify(gateway_key, self.nonce()) } @@ -500,14 +863,30 @@ impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse { fn private_ips(&self) -> IpPair { self.reply.gateway_data.private_ips.into() } + + fn finalise_registration( + &self, + private_key: &x25519::PrivateKey, + credential: Option, + ) -> Box { + Box::new(v5::registration::FinalMessage { + gateway_client: v5::registration::GatewayClient::new( + private_key, + self.pub_key().inner(), + self.reply.gateway_data.private_ips, + self.nonce(), + ), + credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)), + }) + } } -impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse { +impl PendingRegistrationResponse for v6::response::PendingRegistrationResponse { fn nonce(&self) -> u64 { self.reply.nonce } - fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> { + fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> { self.reply.gateway_data.verify(gateway_key, self.nonce()) } @@ -518,9 +897,25 @@ impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse { fn private_ips(&self) -> IpPair { self.reply.gateway_data.private_ips } + + fn finalise_registration( + &self, + private_key: &x25519::PrivateKey, + credential: Option, + ) -> Box { + Box::new(v6::registration::FinalMessage { + gateway_client: v6::registration::GatewayClient::new( + private_key, + self.pub_key().inner(), + self.reply.gateway_data.private_ips, + self.nonce(), + ), + credential, + }) + } } -pub trait RegisteredResponse: Id + fmt::Debug { +pub trait RegisteredResponse: Id + UpgradeModeStatus + fmt::Debug { fn private_ips(&self) -> IpPair; fn pub_key(&self) -> PeerPublicKey; fn wg_port(&self) -> u16; @@ -555,7 +950,8 @@ impl RegisteredResponse for v3::response::RegisteredResponse { } impl RegisteredResponse for v4::response::RegisteredResponse { fn private_ips(&self) -> IpPair { - self.reply.private_ips.into() + // v4 -> v5 -> v6 + v5::registration::IpPair::from(self.reply.private_ips).into() } fn pub_key(&self) -> PeerPublicKey { @@ -568,6 +964,20 @@ impl RegisteredResponse for v4::response::RegisteredResponse { } impl RegisteredResponse for v5::response::RegisteredResponse { + fn private_ips(&self) -> IpPair { + self.reply.private_ips.into() + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.pub_key + } + + fn wg_port(&self) -> u16 { + self.reply.wg_port + } +} + +impl RegisteredResponse for v6::response::RegisteredResponse { fn private_ips(&self) -> IpPair { self.reply.private_ips } @@ -581,7 +991,7 @@ impl RegisteredResponse for v5::response::RegisteredResponse { } } -pub trait RemainingBandwidthResponse: Id + fmt::Debug { +pub trait RemainingBandwidthResponse: Id + UpgradeModeStatus + fmt::Debug { fn available_bandwidth(&self) -> Option; } @@ -609,7 +1019,13 @@ impl RemainingBandwidthResponse for v5::response::RemainingBandwidthResponse { } } -pub trait TopUpBandwidthResponse: Id + fmt::Debug { +impl RemainingBandwidthResponse for v6::response::RemainingBandwidthResponse { + fn available_bandwidth(&self) -> Option { + self.reply.as_ref().map(|r| r.available_bandwidth) + } +} + +pub trait TopUpBandwidthResponse: Id + UpgradeModeStatus + fmt::Debug { fn available_bandwidth(&self) -> i64; } @@ -630,3 +1046,9 @@ impl TopUpBandwidthResponse for v5::response::TopUpBandwidthResponse { self.reply.available_bandwidth } } + +impl TopUpBandwidthResponse for v6::response::TopUpBandwidthResponse { + fn available_bandwidth(&self) -> i64 { + self.reply.available_bandwidth + } +} diff --git a/common/authenticator-requests/src/v1/registration.rs b/common/authenticator-requests/src/v1/registration.rs index 6807325921..aa79910972 100644 --- a/common/authenticator-requests/src/v1/registration.rs +++ b/common/authenticator-requests/src/v1/registration.rs @@ -48,7 +48,7 @@ pub struct RegistrationData { } #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct RegistredData { +pub struct RegisteredData { pub pub_key: PeerPublicKey, pub private_ip: IpAddr, pub wg_port: u16, diff --git a/common/authenticator-requests/src/v1/response.rs b/common/authenticator-requests/src/v1/response.rs index 4e7ba61eb4..f4ff8f5da3 100644 --- a/common/authenticator-requests/src/v1/response.rs +++ b/common/authenticator-requests/src/v1/response.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData}; use nym_sphinx::addressing::Recipient; use serde::{Deserialize, Serialize}; @@ -34,7 +34,7 @@ impl AuthenticatorResponse { } pub fn new_registered( - registred_data: RegistredData, + registred_data: RegisteredData, reply_to: Recipient, request_id: u64, ) -> Self { @@ -108,7 +108,7 @@ pub struct PendingRegistrationResponse { pub struct RegisteredResponse { pub request_id: u64, pub reply_to: Recipient, - pub reply: RegistredData, + pub reply: RegisteredData, } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/common/authenticator-requests/src/v2/conversion.rs b/common/authenticator-requests/src/v2/conversion.rs index b8e16ac4ba..36dceaf3ff 100644 --- a/common/authenticator-requests/src/v2/conversion.rs +++ b/common/authenticator-requests/src/v2/conversion.rs @@ -154,8 +154,8 @@ impl From for v1::registration::Registration } } -impl From for v1::registration::RegistredData { - fn from(value: v2::registration::RegistredData) -> Self { +impl From for v1::registration::RegisteredData { + fn from(value: v2::registration::RegisteredData) -> Self { Self { pub_key: value.pub_key, private_ip: value.private_ip, diff --git a/common/authenticator-requests/src/v2/registration.rs b/common/authenticator-requests/src/v2/registration.rs index a8d5f5e089..34d3b7f4e0 100644 --- a/common/authenticator-requests/src/v2/registration.rs +++ b/common/authenticator-requests/src/v2/registration.rs @@ -58,7 +58,7 @@ pub struct RegistrationData { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct RegistredData { +pub struct RegisteredData { pub pub_key: PeerPublicKey, pub private_ip: IpAddr, pub wg_port: u16, diff --git a/common/authenticator-requests/src/v2/response.rs b/common/authenticator-requests/src/v2/response.rs index 1b389de43f..33da1b975d 100644 --- a/common/authenticator-requests/src/v2/response.rs +++ b/common/authenticator-requests/src/v2/response.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData}; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use nym_sphinx::addressing::Recipient; use serde::{Deserialize, Serialize}; @@ -38,7 +38,7 @@ impl AuthenticatorResponse { } pub fn new_registered( - registred_data: RegistredData, + registred_data: RegisteredData, reply_to: Recipient, request_id: u64, ) -> Self { @@ -118,7 +118,7 @@ pub struct PendingRegistrationResponse { pub struct RegisteredResponse { pub request_id: u64, pub reply_to: Recipient, - pub reply: RegistredData, + pub reply: RegisteredData, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] diff --git a/common/authenticator-requests/src/v3/conversion.rs b/common/authenticator-requests/src/v3/conversion.rs index 5a49c771cd..d04bb6134f 100644 --- a/common/authenticator-requests/src/v3/conversion.rs +++ b/common/authenticator-requests/src/v3/conversion.rs @@ -299,8 +299,8 @@ impl From for v3::registration::Registration } } -impl From for v2::registration::RegistredData { - fn from(value: v3::registration::RegistredData) -> Self { +impl From for v2::registration::RegisteredData { + fn from(value: v3::registration::RegisteredData) -> Self { Self { pub_key: value.pub_key, private_ip: value.private_ip, @@ -309,8 +309,8 @@ impl From for v2::registration::RegistredData { } } -impl From for v3::registration::RegistredData { - fn from(value: v2::registration::RegistredData) -> Self { +impl From for v3::registration::RegisteredData { + fn from(value: v2::registration::RegisteredData) -> Self { Self { pub_key: value.pub_key, private_ip: value.private_ip, @@ -674,7 +674,7 @@ mod tests { let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); let private_ip = IpAddr::from_str("10.10.10.10").unwrap(); let wg_port = 51822; - let registred_data = v2::registration::RegistredData { + let registred_data = v2::registration::RegisteredData { pub_key, private_ip, wg_port, @@ -701,7 +701,7 @@ mod tests { v3::response::AuthenticatorResponseData::Registered(v3::response::RegisteredResponse { request_id, reply_to, - reply: v3::registration::RegistredData { + reply: v3::registration::RegisteredData { wg_port, pub_key, private_ip @@ -715,7 +715,7 @@ mod tests { let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); let private_ip = IpAddr::from_str("10.10.10.10").unwrap(); let wg_port = 51822; - let registred_data = v3::registration::RegistredData { + let registred_data = v3::registration::RegisteredData { pub_key, private_ip, wg_port, @@ -742,7 +742,7 @@ mod tests { v2::response::AuthenticatorResponseData::Registered(v2::response::RegisteredResponse { request_id, reply_to, - reply: v2::registration::RegistredData { + reply: v2::registration::RegisteredData { wg_port, pub_key, private_ip diff --git a/common/authenticator-requests/src/v3/registration.rs b/common/authenticator-requests/src/v3/registration.rs index 00cb146772..66b02c5ed2 100644 --- a/common/authenticator-requests/src/v3/registration.rs +++ b/common/authenticator-requests/src/v3/registration.rs @@ -58,7 +58,7 @@ pub struct RegistrationData { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct RegistredData { +pub struct RegisteredData { pub pub_key: PeerPublicKey, pub private_ip: IpAddr, pub wg_port: u16, diff --git a/common/authenticator-requests/src/v3/response.rs b/common/authenticator-requests/src/v3/response.rs index ca44fb19f6..4fd0a9729b 100644 --- a/common/authenticator-requests/src/v3/response.rs +++ b/common/authenticator-requests/src/v3/response.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData}; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use nym_sphinx::addressing::Recipient; use serde::{Deserialize, Serialize}; @@ -38,7 +38,7 @@ impl AuthenticatorResponse { } pub fn new_registered( - registred_data: RegistredData, + registred_data: RegisteredData, reply_to: Recipient, request_id: u64, ) -> Self { @@ -139,7 +139,7 @@ pub struct PendingRegistrationResponse { pub struct RegisteredResponse { pub request_id: u64, pub reply_to: Recipient, - pub reply: RegistredData, + pub reply: RegisteredData, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] diff --git a/common/authenticator-requests/src/v4/conversion.rs b/common/authenticator-requests/src/v4/conversion.rs index 8731222446..3b2cf9a8f2 100644 --- a/common/authenticator-requests/src/v4/conversion.rs +++ b/common/authenticator-requests/src/v4/conversion.rs @@ -262,8 +262,8 @@ impl From for v3::response::TopUpBandwidth } } -impl From for v4::registration::RegistredData { - fn from(value: v3::registration::RegistredData) -> Self { +impl From for v4::registration::RegisteredData { + fn from(value: v3::registration::RegisteredData) -> Self { Self { pub_key: value.pub_key, private_ips: value.private_ip.into(), @@ -272,8 +272,8 @@ impl From for v4::registration::RegistredData { } } -impl From for v3::registration::RegistredData { - fn from(value: v4::registration::RegistredData) -> Self { +impl From for v3::registration::RegisteredData { + fn from(value: v4::registration::RegisteredData) -> Self { Self { pub_key: value.pub_key, private_ip: value.private_ips.ipv4.into(), @@ -565,7 +565,7 @@ mod tests { let private_ips = v4::registration::IpPair::new(ipv4, Ipv6Addr::from_str("fc01::a0a").unwrap()); let wg_port = 51822; - let registred_data = v3::registration::RegistredData { + let registred_data = v3::registration::RegisteredData { pub_key, private_ip: ipv4.into(), wg_port, @@ -592,7 +592,7 @@ mod tests { v4::response::AuthenticatorResponseData::Registered(v4::response::RegisteredResponse { request_id, reply_to, - reply: v4::registration::RegistredData { + reply: v4::registration::RegisteredData { wg_port, pub_key, private_ips @@ -608,7 +608,7 @@ mod tests { let private_ips = v4::registration::IpPair::new(ipv4, Ipv6Addr::from_str("fc01::10").unwrap()); let wg_port = 51822; - let registred_data = v4::registration::RegistredData { + let registred_data = v4::registration::RegisteredData { pub_key, private_ips, wg_port, @@ -635,7 +635,7 @@ mod tests { v3::response::AuthenticatorResponseData::Registered(v3::response::RegisteredResponse { request_id, reply_to, - reply: v3::registration::RegistredData { + reply: v3::registration::RegisteredData { wg_port, pub_key, private_ip: ipv4.into() diff --git a/common/authenticator-requests/src/v4/registration.rs b/common/authenticator-requests/src/v4/registration.rs index a383b79beb..b1ee074dfd 100644 --- a/common/authenticator-requests/src/v4/registration.rs +++ b/common/authenticator-requests/src/v4/registration.rs @@ -110,7 +110,7 @@ pub struct RegistrationData { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct RegistredData { +pub struct RegisteredData { pub pub_key: PeerPublicKey, pub private_ips: IpPair, pub wg_port: u16, diff --git a/common/authenticator-requests/src/v4/response.rs b/common/authenticator-requests/src/v4/response.rs index 9743e8db43..1bbf4557e9 100644 --- a/common/authenticator-requests/src/v4/response.rs +++ b/common/authenticator-requests/src/v4/response.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData}; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use nym_sphinx::addressing::Recipient; use serde::{Deserialize, Serialize}; @@ -38,7 +38,7 @@ impl AuthenticatorResponse { } pub fn new_registered( - registred_data: RegistredData, + registred_data: RegisteredData, reply_to: Recipient, request_id: u64, ) -> Self { @@ -139,7 +139,7 @@ pub struct PendingRegistrationResponse { pub struct RegisteredResponse { pub request_id: u64, pub reply_to: Recipient, - pub reply: RegistredData, + pub reply: RegisteredData, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] diff --git a/common/authenticator-requests/src/v5/conversion.rs b/common/authenticator-requests/src/v5/conversion.rs index 77ed294323..f2287c03bd 100644 --- a/common/authenticator-requests/src/v5/conversion.rs +++ b/common/authenticator-requests/src/v5/conversion.rs @@ -186,8 +186,8 @@ impl From for v5::response::TopUpBandwidth } } -impl From for v5::registration::RegistredData { - fn from(value: v4::registration::RegistredData) -> Self { +impl From for v5::registration::RegisteredData { + fn from(value: v4::registration::RegisteredData) -> Self { Self { pub_key: value.pub_key, private_ips: value.private_ips.into(), @@ -405,7 +405,7 @@ mod tests { let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); let private_ips = v4::registration::IpPair::new(ipv4, ipv6); let wg_port = 51822; - let registred_data = v4::registration::RegistredData { + let registred_data = v4::registration::RegisteredData { pub_key, private_ips, wg_port, @@ -431,7 +431,7 @@ mod tests { upgraded_msg.data, v5::response::AuthenticatorResponseData::Registered(v5::response::RegisteredResponse { request_id, - reply: v5::registration::RegistredData { + reply: v5::registration::RegisteredData { wg_port, pub_key, private_ips: v5::registration::IpPair::new(ipv4, ipv6) diff --git a/common/authenticator-requests/src/v5/registration.rs b/common/authenticator-requests/src/v5/registration.rs index 151401da97..5154400f93 100644 --- a/common/authenticator-requests/src/v5/registration.rs +++ b/common/authenticator-requests/src/v5/registration.rs @@ -108,7 +108,7 @@ pub struct RegistrationData { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct RegistredData { +pub struct RegisteredData { pub pub_key: PeerPublicKey, pub private_ips: IpPair, pub wg_port: u16, diff --git a/common/authenticator-requests/src/v5/response.rs b/common/authenticator-requests/src/v5/response.rs index 044b803d0d..b26fcf4627 100644 --- a/common/authenticator-requests/src/v5/response.rs +++ b/common/authenticator-requests/src/v5/response.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData}; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use serde::{Deserialize, Serialize}; @@ -32,7 +32,7 @@ impl AuthenticatorResponse { } } - pub fn new_registered(registred_data: RegistredData, request_id: u64) -> Self { + pub fn new_registered(registred_data: RegisteredData, request_id: u64) -> Self { Self { protocol: Protocol { service_provider_type: ServiceProviderType::Authenticator, @@ -116,7 +116,7 @@ pub struct PendingRegistrationResponse { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct RegisteredResponse { pub request_id: u64, - pub reply: RegistredData, + pub reply: RegisteredData, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] diff --git a/common/authenticator-requests/src/v6/conversion.rs b/common/authenticator-requests/src/v6/conversion.rs new file mode 100644 index 0000000000..8bcc204d7c --- /dev/null +++ b/common/authenticator-requests/src/v6/conversion.rs @@ -0,0 +1,441 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{v5, v6}; + +impl TryFrom for v6::request::AuthenticatorRequest { + type Error = crate::Error; + + fn try_from( + authenticator_request: v5::request::AuthenticatorRequest, + ) -> Result { + Ok(Self { + protocol: v6::PROTOCOL, + data: authenticator_request.data.try_into()?, + request_id: authenticator_request.request_id, + }) + } +} + +impl TryFrom for v6::request::AuthenticatorRequestData { + type Error = crate::Error; + + fn try_from( + authenticator_request_data: v5::request::AuthenticatorRequestData, + ) -> Result { + match authenticator_request_data { + v5::request::AuthenticatorRequestData::Initial(init_msg) => Ok( + v6::request::AuthenticatorRequestData::Initial(init_msg.into()), + ), + v5::request::AuthenticatorRequestData::Final(final_msg) => Ok( + v6::request::AuthenticatorRequestData::Final(Box::new((*final_msg).try_into()?)), + ), + v5::request::AuthenticatorRequestData::QueryBandwidth(pub_key) => Ok( + v6::request::AuthenticatorRequestData::QueryBandwidth(pub_key), + ), + v5::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => Ok( + v6::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message.into()), + ), + } + } +} + +impl From for v6::registration::InitMessage { + fn from(init_msg: v5::registration::InitMessage) -> Self { + Self { + pub_key: init_msg.pub_key, + } + } +} + +impl TryFrom for v6::registration::FinalMessage { + type Error = crate::Error; + + fn try_from(final_msg: v5::registration::FinalMessage) -> Result { + Ok(Self { + gateway_client: final_msg.gateway_client.into(), + credential: final_msg + .credential + .map(TryInto::try_into) + .transpose() + .map_err(Self::Error::conversion_display)?, + }) + } +} + +impl From for v6::registration::GatewayClient { + fn from(gateway_client: v5::registration::GatewayClient) -> Self { + Self { + pub_key: gateway_client.pub_key, + private_ips: gateway_client.private_ips.into(), + mac: gateway_client.mac.into(), + } + } +} + +impl From for v5::registration::GatewayClient { + fn from(gateway_client: v6::registration::GatewayClient) -> Self { + Self { + pub_key: gateway_client.pub_key, + private_ips: gateway_client.private_ips.into(), + mac: gateway_client.mac.into(), + } + } +} + +impl From for v6::registration::ClientMac { + fn from(client_mac: v5::registration::ClientMac) -> Self { + Self::new((*client_mac).clone()) + } +} + +impl From for v5::registration::ClientMac { + fn from(client_mac: v6::registration::ClientMac) -> Self { + Self::new((*client_mac).clone()) + } +} + +impl From> for Box { + fn from(top_up_message: Box) -> Self { + Box::new(v6::topup::TopUpMessage { + pub_key: top_up_message.pub_key, + credential: top_up_message.credential, + }) + } +} + +impl From for v6::response::AuthenticatorResponse { + fn from(value: v5::response::AuthenticatorResponse) -> Self { + Self { + protocol: v6::PROTOCOL, + data: value.data.into(), + } + } +} + +impl From for v6::response::AuthenticatorResponseData { + fn from(authenticator_response_data: v5::response::AuthenticatorResponseData) -> Self { + match authenticator_response_data { + v5::response::AuthenticatorResponseData::PendingRegistration(pending_response) => { + v6::response::AuthenticatorResponseData::PendingRegistration( + pending_response.into(), + ) + } + v5::response::AuthenticatorResponseData::Registered(registered_response) => { + v6::response::AuthenticatorResponseData::Registered(registered_response.into()) + } + v5::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => v6::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response.into(), + ), + v5::response::AuthenticatorResponseData::TopUpBandwidth(top_up_response) => { + v6::response::AuthenticatorResponseData::TopUpBandwidth(top_up_response.into()) + } + } + } +} + +impl From for v6::response::RegisteredResponse { + fn from(value: v5::response::RegisteredResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.into(), + upgrade_mode_enabled: false, + } + } +} + +impl From for v6::response::PendingRegistrationResponse { + fn from(value: v5::response::PendingRegistrationResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.into(), + upgrade_mode_enabled: false, + } + } +} + +impl From for v6::registration::RegistrationData { + fn from(value: v5::registration::RegistrationData) -> Self { + Self { + nonce: value.nonce, + gateway_data: value.gateway_data.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v5::registration::RegistrationData { + fn from(value: v6::registration::RegistrationData) -> Self { + Self { + nonce: value.nonce, + gateway_data: value.gateway_data.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v6::response::RemainingBandwidthResponse { + fn from(value: v5::response::RemainingBandwidthResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.map(Into::into), + upgrade_mode_enabled: false, + } + } +} + +impl From for v6::response::TopUpBandwidthResponse { + fn from(value: v5::response::TopUpBandwidthResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.into(), + upgrade_mode_enabled: false, + } + } +} + +impl From for v6::registration::RegisteredData { + fn from(value: v5::registration::RegisteredData) -> Self { + Self { + pub_key: value.pub_key, + private_ips: value.private_ips.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v6::registration::RemainingBandwidthData { + fn from(value: v5::registration::RemainingBandwidthData) -> Self { + Self { + available_bandwidth: value.available_bandwidth, + } + } +} + +impl From for v6::registration::IpPair { + fn from(value: v5::registration::IpPair) -> Self { + Self { + ipv4: value.ipv4, + ipv6: value.ipv6, + } + } +} + +impl From for v5::registration::IpPair { + fn from(value: v6::registration::IpPair) -> Self { + Self { + ipv4: value.ipv4, + ipv6: value.ipv6, + } + } +} + +#[cfg(test)] +mod tests { + use std::{ + net::{Ipv4Addr, Ipv6Addr}, + str::FromStr, + }; + + use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData, TicketType}; + use nym_crypto::asymmetric::x25519::PrivateKey; + use nym_wireguard_types::PeerPublicKey; + use x25519_dalek::PublicKey; + + use super::*; + use crate::models::BandwidthClaim; + use crate::{util::tests::CREDENTIAL_BYTES, v5}; + + #[test] + fn upgrade_initial_req() { + let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); + + let (msg, _) = v5::request::AuthenticatorRequest::new_initial_request( + v5::registration::InitMessage::new(pub_key), + ); + let upgraded_msg = v6::request::AuthenticatorRequest::try_from(msg).unwrap(); + + assert_eq!(upgraded_msg.protocol, v6::PROTOCOL); + assert_eq!( + upgraded_msg.data, + v6::request::AuthenticatorRequestData::Initial(v6::registration::InitMessage { + pub_key + }) + ); + } + + #[test] + fn upgrade_final_req() { + let mut rng = rand::thread_rng(); + + let local_secret = PrivateKey::new(&mut rng); + let remote_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng); + let ipv4 = Ipv4Addr::from_str("10.10.10.10").unwrap(); + let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); + let ips = v5::registration::IpPair::new(ipv4, ipv6); + let nonce = 42; + let gateway_client = v5::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + ips, + nonce, + ); + let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(); + let final_message = v5::registration::FinalMessage { + gateway_client: gateway_client.clone(), + credential: Some(credential.clone()), + }; + + let (msg, _) = v5::request::AuthenticatorRequest::new_final_request(final_message); + let upgraded_msg = v6::request::AuthenticatorRequest::try_from(msg).unwrap(); + + assert_eq!(upgraded_msg.protocol, v6::PROTOCOL); + assert_eq!( + upgraded_msg.data, + v6::request::AuthenticatorRequestData::Final(Box::new( + v6::registration::FinalMessage { + gateway_client: v6::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + v6::registration::IpPair::new(ipv4, ipv6), + nonce + ), + credential: Some(BandwidthClaim { + credential: BandwidthCredential::ZkNym(Box::new(credential)), + kind: TicketType::V1MixnetEntry, + }) + } + )) + ); + } + + #[test] + fn upgrade_query_req() { + let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); + + let (msg, _) = v5::request::AuthenticatorRequest::new_query_request(pub_key); + let upgraded_msg = v6::request::AuthenticatorRequest::try_from(msg).unwrap(); + + assert_eq!(upgraded_msg.protocol, v6::PROTOCOL); + assert_eq!( + upgraded_msg.data, + v6::request::AuthenticatorRequestData::QueryBandwidth(pub_key) + ); + } + + #[test] + fn upgrade_pending_reg_resp() { + let mut rng = rand::thread_rng(); + + let local_secret = PrivateKey::new(&mut rng); + let remote_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng); + let ipv4 = Ipv4Addr::from_str("10.10.10.10").unwrap(); + let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); + let ips = v5::registration::IpPair::new(ipv4, ipv6); + let nonce = 42; + let wg_port = 51822; + let gateway_data = v5::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + ips, + nonce, + ); + let registration_data = v5::registration::RegistrationData { + nonce, + gateway_data, + wg_port, + }; + let request_id = 123; + + let msg = v5::response::AuthenticatorResponse::new_pending_registration_success( + registration_data, + request_id, + ); + let upgraded_msg = v6::response::AuthenticatorResponse::from(msg); + + assert_eq!(upgraded_msg.protocol, v6::PROTOCOL); + + assert_eq!( + upgraded_msg.data, + v6::response::AuthenticatorResponseData::PendingRegistration( + v6::response::PendingRegistrationResponse { + request_id, + reply: v6::registration::RegistrationData { + nonce, + gateway_data: v6::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + v6::registration::IpPair::new(ipv4, ipv6), + nonce + ), + wg_port + }, + upgrade_mode_enabled: false, + } + ) + ); + } + + #[test] + fn upgrade_registered_resp() { + let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); + let ipv4 = Ipv4Addr::from_str("10.1.10.10").unwrap(); + let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); + let private_ips = v5::registration::IpPair::new(ipv4, ipv6); + let wg_port = 51822; + let registered_data = v5::registration::RegisteredData { + pub_key, + private_ips, + wg_port, + }; + let request_id = 123; + + let msg = v5::response::AuthenticatorResponse::new_registered(registered_data, request_id); + let upgraded_msg = v6::response::AuthenticatorResponse::from(msg); + + assert_eq!(upgraded_msg.protocol, v6::PROTOCOL); + assert_eq!( + upgraded_msg.data, + v6::response::AuthenticatorResponseData::Registered(v6::response::RegisteredResponse { + request_id, + reply: v6::registration::RegisteredData { + wg_port, + pub_key, + private_ips: v6::registration::IpPair::new(ipv4, ipv6) + }, + upgrade_mode_enabled: false, + }) + ); + } + + #[test] + fn upgrade_remaining_bandwidth_resp() { + let available_bandwidth = 42; + let remaining_bandwidth_data = Some(v5::registration::RemainingBandwidthData { + available_bandwidth, + }); + let request_id = 123; + + let msg = v5::response::AuthenticatorResponse::new_remaining_bandwidth( + remaining_bandwidth_data, + request_id, + ); + let upgraded_msg = v6::response::AuthenticatorResponse::from(msg); + + assert_eq!(upgraded_msg.protocol, v6::PROTOCOL); + assert_eq!( + upgraded_msg.data, + v6::response::AuthenticatorResponseData::RemainingBandwidth( + v6::response::RemainingBandwidthResponse { + request_id, + reply: Some(v6::registration::RemainingBandwidthData { + available_bandwidth, + }), + upgrade_mode_enabled: false, + } + ) + ); + } +} diff --git a/common/authenticator-requests/src/v6/mod.rs b/common/authenticator-requests/src/v6/mod.rs new file mode 100644 index 0000000000..6fbc095ae9 --- /dev/null +++ b/common/authenticator-requests/src/v6/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; + +pub mod conversion; +pub mod registration; +pub mod request; +pub mod response; +pub mod topup; +pub mod upgrade_mode_check; + +pub const VERSION: u8 = 6; + +pub const PROTOCOL: Protocol = Protocol::new(VERSION, ServiceProviderType::Authenticator); diff --git a/common/authenticator-requests/src/v6/registration.rs b/common/authenticator-requests/src/v6/registration.rs new file mode 100644 index 0000000000..11fcf34116 --- /dev/null +++ b/common/authenticator-requests/src/v6/registration.rs @@ -0,0 +1,287 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::Error; +use crate::models::BandwidthClaim; +use base64::{Engine, engine::general_purpose}; +use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6}; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::time::SystemTime; +use std::{fmt, ops::Deref, str::FromStr}; + +#[cfg(feature = "verify")] +use hmac::{Hmac, Mac}; +#[cfg(feature = "verify")] +use nym_crypto::asymmetric::x25519::{PrivateKey, PublicKey}; +#[cfg(feature = "verify")] +use sha2::Sha256; + +pub type PendingRegistrations = HashMap; +pub type PrivateIPs = HashMap; + +#[cfg(feature = "verify")] +pub type HmacSha256 = Hmac; + +pub type Nonce = u64; +pub type Taken = Option; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct IpPair { + pub ipv4: Ipv4Addr, + pub ipv6: Ipv6Addr, +} + +impl IpPair { + pub fn new(ipv4: Ipv4Addr, ipv6: Ipv6Addr) -> Self { + IpPair { ipv4, ipv6 } + } +} + +impl From<(Ipv4Addr, Ipv6Addr)> for IpPair { + fn from((ipv4, ipv6): (Ipv4Addr, Ipv6Addr)) -> Self { + IpPair { ipv4, ipv6 } + } +} + +impl fmt::Display for IpPair { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "({}, {})", self.ipv4, self.ipv6) + } +} + +impl From for IpPair { + fn from(value: IpAddr) -> Self { + let (before_last_byte, last_byte) = match value { + IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]), + IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]), + }; + let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16; + let ipv4 = Ipv4Addr::new( + WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0], + WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1], + before_last_byte, + last_byte, + ); + let ipv6 = Ipv6Addr::new( + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6], + last_bytes, + ); + IpPair::new(ipv4, ipv6) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InitMessage { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, +} + +impl InitMessage { + pub fn new(pub_key: PeerPublicKey) -> Self { + InitMessage { pub_key } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct FinalMessage { + /// Gateway client data + pub gateway_client: GatewayClient, + + /// Ecash credential + pub credential: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RegistrationData { + pub nonce: u64, + pub gateway_data: GatewayClient, + pub wg_port: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RegisteredData { + pub pub_key: PeerPublicKey, + pub private_ips: IpPair, + pub wg_port: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RemainingBandwidthData { + pub available_bandwidth: i64, +} + +/// Client that wants to register sends its PublicKey bytes mac digest encrypted with a DH shared secret. +/// Gateway/Nym node can then verify pub_key payload using the same process +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct GatewayClient { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, + + /// Assigned private IPs (v4 and v6) + pub private_ips: IpPair, + + /// Sha256 hmac on the data (alongside the prior nonce) + pub mac: ClientMac, +} + +impl GatewayClient { + #[cfg(feature = "verify")] + pub fn new( + local_secret: &PrivateKey, + remote_public: x25519_dalek::PublicKey, + private_ips: IpPair, + nonce: u64, + ) -> Self { + let local_public = PublicKey::from(local_secret); + let remote_public = PublicKey::from(remote_public); + + let dh = local_secret.diffie_hellman(&remote_public); + + // TODO: change that to use our nym_crypto::hmac module instead + #[allow(clippy::expect_used)] + let mut mac = HmacSha256::new_from_slice(&dh[..]) + .expect("x25519 shared secret is always 32 bytes long"); + + mac.update(local_public.as_bytes()); + mac.update(private_ips.to_string().as_bytes()); + mac.update(&nonce.to_le_bytes()); + + GatewayClient { + pub_key: PeerPublicKey::new(local_public.into()), + private_ips, + mac: ClientMac(mac.finalize().into_bytes().to_vec()), + } + } + + // Reusable secret should be gateways Wireguard PK + // Client should perform this step when generating its payload, using its own WG PK + #[cfg(feature = "verify")] + pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + // use gateways key as a ref to an x25519_dalek key + let dh = gateway_key.inner().diffie_hellman(&self.pub_key); + + // TODO: change that to use our nym_crypto::hmac module instead + #[allow(clippy::expect_used)] + let mut mac = HmacSha256::new_from_slice(dh.as_bytes()) + .expect("x25519 shared secret is always 32 bytes long"); + + mac.update(self.pub_key.as_bytes()); + mac.update(self.private_ips.to_string().as_bytes()); + mac.update(&nonce.to_le_bytes()); + + mac.verify_slice(&self.mac) + .map_err(|source| Error::FailedClientMacVerification { + client: self.pub_key.to_string(), + source, + }) + } + + pub fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +// TODO: change the inner type into generic array of size HmacSha256::OutputSize +// TODO2: rely on our internal crypto/hmac +#[derive(Debug, Clone, PartialEq)] +pub struct ClientMac(Vec); + +impl fmt::Display for ClientMac { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", general_purpose::STANDARD.encode(&self.0)) + } +} + +impl From> for ClientMac { + fn from(v: Vec) -> Self { + ClientMac(v) + } +} + +impl ClientMac { + #[allow(dead_code)] + pub fn new(mac: Vec) -> Self { + ClientMac(mac) + } +} + +impl Deref for ClientMac { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromStr for ClientMac { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mac_bytes: Vec = + general_purpose::STANDARD + .decode(s) + .map_err(|source| Error::MalformedClientMac { + mac: s.to_string(), + source, + })?; + + Ok(ClientMac(mac_bytes)) + } +} + +impl Serialize for ClientMac { + fn serialize(&self, serializer: S) -> Result { + let encoded_key = general_purpose::STANDARD.encode(self.0.clone()); + serializer.serialize_str(&encoded_key) + } +} + +impl<'de> Deserialize<'de> for ClientMac { + fn deserialize>(deserializer: D) -> Result { + let encoded_key = String::deserialize(deserializer)?; + ClientMac::from_str(&encoded_key).map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_crypto::asymmetric::x25519; + use nym_test_utils::helpers::deterministic_rng; + + #[test] + fn create_ip_pair() { + let ipv4: IpAddr = Ipv4Addr::from_str("10.1.10.50").unwrap().into(); + let ipv6: IpAddr = Ipv6Addr::from_str("fc01::0a32").unwrap().into(); + + assert_eq!(IpPair::from(ipv4), IpPair::from(ipv6)); + } + + #[test] + #[cfg(feature = "verify")] + fn client_request_roundtrip() { + let mut rng = deterministic_rng(); + + let gateway_key_pair = x25519::KeyPair::new(&mut rng); + let client_key_pair = x25519::KeyPair::new(&mut rng); + + let nonce = 1234567890; + + let client = GatewayClient::new( + client_key_pair.private_key(), + x25519_dalek::PublicKey::from(gateway_key_pair.public_key().to_bytes()), + IpPair::new("10.0.0.42".parse().unwrap(), "fc00::42".parse().unwrap()), + nonce, + ); + assert!(client.verify(gateway_key_pair.private_key(), nonce).is_ok()) + } +} diff --git a/common/authenticator-requests/src/v6/request.rs b/common/authenticator-requests/src/v6/request.rs new file mode 100644 index 0000000000..3bc8140b74 --- /dev/null +++ b/common/authenticator-requests/src/v6/request.rs @@ -0,0 +1,135 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::{ + PROTOCOL, + registration::{FinalMessage, InitMessage}, + topup::TopUpMessage, + upgrade_mode_check::UpgradeModeCheckRequest, +}; +use nym_service_provider_requests_common::Protocol; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; + +use crate::make_bincode_serializer; + +fn generate_random() -> u64 { + use rand::RngCore; + let mut rng = rand::rngs::OsRng; + rng.next_u64() +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct AuthenticatorRequest { + pub protocol: Protocol, + pub data: AuthenticatorRequestData, + pub request_id: u64, +} + +impl AuthenticatorRequest { + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + make_bincode_serializer().deserialize(&message.message) + } + + pub fn new_initial_request(init_message: InitMessage) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: PROTOCOL, + data: AuthenticatorRequestData::Initial(init_message), + request_id, + }, + request_id, + ) + } + + pub fn new_final_request(final_message: FinalMessage) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: PROTOCOL, + data: AuthenticatorRequestData::Final(Box::new(final_message)), + request_id, + }, + request_id, + ) + } + + pub fn new_query_request(peer_public_key: PeerPublicKey) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: PROTOCOL, + data: AuthenticatorRequestData::QueryBandwidth(peer_public_key), + request_id, + }, + request_id, + ) + } + + pub fn new_topup_request(top_up_message: TopUpMessage) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: PROTOCOL, + data: AuthenticatorRequestData::TopUpBandwidth(Box::new(top_up_message)), + request_id, + }, + request_id, + ) + } + + pub fn new_upgrade_mode_check_request(message: UpgradeModeCheckRequest) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: PROTOCOL, + data: AuthenticatorRequestData::CheckUpgradeMode(message), + request_id, + }, + request_id, + ) + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum AuthenticatorRequestData { + Initial(InitMessage), + Final(Box), + QueryBandwidth(PeerPublicKey), + TopUpBandwidth(Box), + CheckUpgradeMode(UpgradeModeCheckRequest), +} + +#[cfg(test)] +mod tests { + use super::super::VERSION; + use super::*; + use nym_service_provider_requests_common::ServiceProviderType; + use std::str::FromStr; + + #[test] + fn check_first_bytes_protocol() { + let version = VERSION; + let data = AuthenticatorRequest { + protocol: Protocol { + version, + service_provider_type: ServiceProviderType::Authenticator, + }, + data: AuthenticatorRequestData::Initial(InitMessage::new( + PeerPublicKey::from_str("yvNUDpT5l7W/xDhiu6HkqTHDQwbs/B3J5UrLmORl1EQ=").unwrap(), + )), + request_id: 1, + }; + let bytes = *data.to_bytes().unwrap().first_chunk::<2>().unwrap(); + assert_eq!(bytes, [version, ServiceProviderType::Authenticator as u8]); + } +} diff --git a/common/authenticator-requests/src/v6/response.rs b/common/authenticator-requests/src/v6/response.rs new file mode 100644 index 0000000000..c93c34e1c3 --- /dev/null +++ b/common/authenticator-requests/src/v6/response.rs @@ -0,0 +1,153 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData}; +use nym_service_provider_requests_common::Protocol; +use serde::{Deserialize, Serialize}; + +use crate::make_bincode_serializer; + +use super::PROTOCOL; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct AuthenticatorResponse { + pub protocol: Protocol, + pub data: AuthenticatorResponseData, +} + +impl AuthenticatorResponse { + pub fn new_pending_registration_success( + registration_data: RegistrationData, + request_id: u64, + upgrade_mode_enabled: bool, + ) -> Self { + Self { + protocol: PROTOCOL, + data: AuthenticatorResponseData::PendingRegistration(PendingRegistrationResponse { + reply: registration_data, + request_id, + upgrade_mode_enabled, + }), + } + } + + pub fn new_registered( + registered_data: RegisteredData, + request_id: u64, + upgrade_mode_enabled: bool, + ) -> Self { + Self { + protocol: PROTOCOL, + data: AuthenticatorResponseData::Registered(RegisteredResponse { + reply: registered_data, + request_id, + upgrade_mode_enabled, + }), + } + } + + pub fn new_remaining_bandwidth( + remaining_bandwidth_data: Option, + request_id: u64, + upgrade_mode_enabled: bool, + ) -> Self { + Self { + protocol: PROTOCOL, + data: AuthenticatorResponseData::RemainingBandwidth(RemainingBandwidthResponse { + reply: remaining_bandwidth_data, + request_id, + upgrade_mode_enabled, + }), + } + } + + pub fn new_topup_bandwidth( + remaining_bandwidth_data: RemainingBandwidthData, + request_id: u64, + upgrade_mode_enabled: bool, + ) -> Self { + Self { + protocol: PROTOCOL, + data: AuthenticatorResponseData::TopUpBandwidth(TopUpBandwidthResponse { + reply: remaining_bandwidth_data, + request_id, + upgrade_mode_enabled, + }), + } + } + + pub fn new_upgrade_mode_check(request_id: u64, upgrade_mode_enabled: bool) -> Self { + Self { + protocol: PROTOCOL, + data: AuthenticatorResponseData::UpgradeMode(UpgradeModeResponse { + request_id, + upgrade_mode_enabled, + }), + } + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } + + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + make_bincode_serializer().deserialize(&message.message) + } + + pub fn id(&self) -> Option { + match &self.data { + AuthenticatorResponseData::PendingRegistration(response) => Some(response.request_id), + AuthenticatorResponseData::Registered(response) => Some(response.request_id), + AuthenticatorResponseData::RemainingBandwidth(response) => Some(response.request_id), + AuthenticatorResponseData::TopUpBandwidth(response) => Some(response.request_id), + AuthenticatorResponseData::UpgradeMode(response) => Some(response.request_id), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum AuthenticatorResponseData { + PendingRegistration(PendingRegistrationResponse), + Registered(RegisteredResponse), + RemainingBandwidth(RemainingBandwidthResponse), + TopUpBandwidth(TopUpBandwidthResponse), + UpgradeMode(UpgradeModeResponse), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct PendingRegistrationResponse { + pub request_id: u64, + pub reply: RegistrationData, + pub upgrade_mode_enabled: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct RegisteredResponse { + pub request_id: u64, + pub reply: RegisteredData, + pub upgrade_mode_enabled: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct RemainingBandwidthResponse { + pub request_id: u64, + pub reply: Option, + pub upgrade_mode_enabled: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct TopUpBandwidthResponse { + pub request_id: u64, + pub reply: RemainingBandwidthData, + pub upgrade_mode_enabled: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct UpgradeModeResponse { + pub request_id: u64, + pub upgrade_mode_enabled: bool, +} diff --git a/common/authenticator-requests/src/v6/topup.rs b/common/authenticator-requests/src/v6/topup.rs new file mode 100644 index 0000000000..b5d25a9dbf --- /dev/null +++ b/common/authenticator-requests/src/v6/topup.rs @@ -0,0 +1,15 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct TopUpMessage { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, + + /// Ecash credential + pub credential: CredentialSpendingData, +} diff --git a/common/authenticator-requests/src/v6/upgrade_mode_check.rs b/common/authenticator-requests/src/v6/upgrade_mode_check.rs new file mode 100644 index 0000000000..ae27af3800 --- /dev/null +++ b/common/authenticator-requests/src/v6/upgrade_mode_check.rs @@ -0,0 +1,12 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[non_exhaustive] +pub enum UpgradeModeCheckRequest { + /// Attempt to request upgrade mode recheck via the JWT issued as the result of + /// global attestation.json being published + UpgradeModeJwt { token: String }, +} diff --git a/common/authenticator-requests/src/version.rs b/common/authenticator-requests/src/version.rs index 4bb8b6d591..d0f2bbf217 100644 --- a/common/authenticator-requests/src/version.rs +++ b/common/authenticator-requests/src/version.rs @@ -1,7 +1,7 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::{v1, v2, v3, v4, v5}; +use super::{v1, v2, v3, v4, v5, v6}; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; #[derive(Copy, Clone, Debug, PartialEq, strum_macros::Display)] @@ -22,11 +22,15 @@ pub enum AuthenticatorVersion { /// introduced in dorina-patched release (1.6.1) V5, + /// introduced in niolo release (1.23.0) + V6, + + /// an unknown, future, variant that can be present if running outdated software UNKNOWN, } impl AuthenticatorVersion { - pub const LATEST: Self = Self::V5; + pub const LATEST: Self = Self::V6; pub const fn release_version(&self) -> semver::Version { match self { @@ -35,6 +39,7 @@ impl AuthenticatorVersion { AuthenticatorVersion::V3 => semver::Version::new(1, 1, 10), AuthenticatorVersion::V4 => semver::Version::new(1, 2, 0), AuthenticatorVersion::V5 => semver::Version::new(1, 6, 1), + AuthenticatorVersion::V6 => semver::Version::new(1, 23, 0), AuthenticatorVersion::UNKNOWN => semver::Version::new(0, 0, 0), } } @@ -54,6 +59,8 @@ impl From for AuthenticatorVersion { AuthenticatorVersion::V4 } else if value.version == v5::VERSION { AuthenticatorVersion::V5 + } else if value.version == v6::VERSION { + AuthenticatorVersion::V6 } else { AuthenticatorVersion::UNKNOWN } @@ -72,6 +79,8 @@ impl From for AuthenticatorVersion { AuthenticatorVersion::V4 } else if value == v5::VERSION { AuthenticatorVersion::V5 + } else if value == v6::VERSION { + AuthenticatorVersion::V6 } else { AuthenticatorVersion::UNKNOWN } @@ -126,11 +135,14 @@ impl From for AuthenticatorVersion { if semver < AuthenticatorVersion::V5.release_version() { return Self::V4; } - // if provided version is higher (or equal) to release version of V5, - // we return the latest (i.e. v5) + if semver < AuthenticatorVersion::V6.release_version() { + return Self::V5; + } + // if provided version is higher (or equal) to release version of V6, + // we return the latest (i.e. v6) debug_assert_eq!( - Self::V5, + Self::V6, Self::LATEST, "a new AuthenticatorVersion variant has been introduced without adjusting the `From` trait" ); @@ -191,5 +203,9 @@ mod tests { assert_eq!(AuthenticatorVersion::V5, "1.7.0".into()); assert_eq!(AuthenticatorVersion::V5, "1.16.11".into()); assert_eq!(AuthenticatorVersion::V5, "1.17.0".into()); + assert_eq!(AuthenticatorVersion::V5, "1.22.0".into()); + assert_eq!(AuthenticatorVersion::V6, "1.23.0".into()); + assert_eq!(AuthenticatorVersion::V6, "1.23.1".into()); + assert_eq!(AuthenticatorVersion::V6, "1.24.0".into()); } } diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index efc699473a..969d94807e 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -88,3 +88,6 @@ features = ["js"] [features] wasm = [] + +[lints] +workspace = true \ No newline at end of file diff --git a/common/client-libs/gateway-client/src/bandwidth.rs b/common/client-libs/gateway-client/src/bandwidth.rs index 9fd43765bd..304eff6c96 100644 --- a/common/client-libs/gateway-client/src/bandwidth.rs +++ b/common/client-libs/gateway-client/src/bandwidth.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use si_scale::helpers::bibytes2; +use std::fmt::{Display, Formatter}; use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -26,6 +27,39 @@ pub struct ClientBandwidth { inner: Arc, } +// simple helper for logging purposes to accommodate 'unknown' case +pub(crate) enum UpgradeModeEnabledWrapper { + True, + False, + Unknown, +} + +impl From> for UpgradeModeEnabledWrapper { + fn from(value: Option) -> Self { + match value { + Some(true) => UpgradeModeEnabledWrapper::True, + Some(false) => UpgradeModeEnabledWrapper::False, + None => UpgradeModeEnabledWrapper::Unknown, + } + } +} + +impl From for UpgradeModeEnabledWrapper { + fn from(value: bool) -> Self { + Some(value).into() + } +} + +impl Display for UpgradeModeEnabledWrapper { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + UpgradeModeEnabledWrapper::True => write!(f, "true"), + UpgradeModeEnabledWrapper::False => write!(f, "false"), + UpgradeModeEnabledWrapper::Unknown => write!(f, "unknown"), + } + } +} + struct ClientBandwidthInner { /// the actual bandwidth amount (in bytes) available available: AtomicI64, @@ -71,26 +105,41 @@ impl ClientBandwidth { self.inner.available.load(Ordering::Acquire) } - pub(crate) fn maybe_log_bandwidth(&self, now: Option) { + pub(crate) fn maybe_log_bandwidth( + &self, + now: Option, + upgrade_mode: impl Into, + ) { let last = self.last_logged(); let now = now.unwrap_or_else(OffsetDateTime::now_utc); if last + Duration::from_secs(10) < now { - self.log_bandwidth(Some(now)) + self.log_bandwidth(Some(now), upgrade_mode) } } - pub(crate) fn log_bandwidth(&self, now: Option) { + pub(crate) fn log_bandwidth( + &self, + now: Option, + upgrade_mode: impl Into, + ) { let now = now.unwrap_or_else(OffsetDateTime::now_utc); + let upgrade_mode = upgrade_mode.into(); let remaining = self.remaining(); let remaining_bi2 = bibytes2(remaining as f64); if remaining < 0 { - tracing::warn!("OUT OF BANDWIDTH. remaining: {remaining_bi2}"); + tracing::warn!( + "OUT OF BANDWIDTH. remaining: {remaining_bi2}. in 'upgrade mode': {upgrade_mode}" + ); } else if remaining < 1_000_000 { - tracing::info!("remaining bandwidth: {remaining_bi2}"); + tracing::info!( + "remaining bandwidth: {remaining_bi2}. in 'upgrade mode': {upgrade_mode}" + ); } else { - tracing::debug!("remaining bandwidth: {remaining_bi2}"); + tracing::trace!( + "remaining bandwidth: {remaining_bi2}. in 'upgrade mode': {upgrade_mode}" + ); } self.inner @@ -98,26 +147,35 @@ impl ClientBandwidth { .store(now.unix_timestamp(), Ordering::Relaxed) } - pub(crate) fn update_and_maybe_log(&self, remaining: i64) { + pub(crate) fn update_and_maybe_log( + &self, + remaining: i64, + upgrade_mode: impl Into, + ) { let now = OffsetDateTime::now_utc(); self.inner.available.store(remaining, Ordering::Release); self.inner .last_updated_ts .store(now.unix_timestamp(), Ordering::Relaxed); - self.maybe_log_bandwidth(Some(now)) + self.maybe_log_bandwidth(Some(now), upgrade_mode) } - pub(crate) fn update_and_log(&self, remaining: i64) { + pub(crate) fn update_and_log( + &self, + remaining: i64, + upgrade_mode: impl Into, + ) { let now = OffsetDateTime::now_utc(); self.inner.available.store(remaining, Ordering::Release); self.inner .last_updated_ts .store(now.unix_timestamp(), Ordering::Relaxed); - self.log_bandwidth(Some(now)) + self.log_bandwidth(Some(now), upgrade_mode) } fn last_logged(&self) -> OffsetDateTime { // SAFETY: this value is always populated with valid timestamps + #[allow(clippy::unwrap_used)] OffsetDateTime::from_unix_timestamp(self.inner.last_logged_ts.load(Ordering::Relaxed)) .unwrap() } diff --git a/common/client-libs/gateway-client/src/client/config.rs b/common/client-libs/gateway-client/src/client/config.rs index fd7bfc142d..af24a72b90 100644 --- a/common/client-libs/gateway-client/src/client/config.rs +++ b/common/client-libs/gateway-client/src/client/config.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::GatewayClientError; -use nym_network_defaults::TicketTypeRepr::V1MixnetEntry; +use nym_credentials_interface::DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD; use si_scale::helpers::bibytes2; use std::time::Duration; @@ -103,7 +103,7 @@ impl BandwidthTickets { // 20% of entry ticket value pub const DEFAULT_REMAINING_BANDWIDTH_THRESHOLD: i64 = - (V1MixnetEntry.bandwidth_value() / 5) as i64; + DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD; pub const DEFAULT_CUTOFF_REMAINING_BANDWIDTH_THRESHOLD: Option = None; diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index 52e5833eb2..e9d91beeb9 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -20,9 +20,9 @@ use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::ed25519; use nym_gateway_requests::registration::handshake::client_handshake; use nym_gateway_requests::{ - BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersionExt, - GatewayRequestsError, SensitiveServerResponse, ServerResponse, SharedGatewayKey, - SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION, + BandwidthResponse, BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersion, + GatewayProtocolVersionExt, GatewayRequestsError, SensitiveServerResponse, ServerResponse, + SharedGatewayKey, SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, }; use nym_sphinx::forwarding::packet::MixPacket; use nym_statistics_common::clients::connection::ConnectionStatsEvent; @@ -101,8 +101,7 @@ pub struct GatewayClient { bandwidth_controller: Option>, stats_reporter: ClientStatsSender, - // currently unused (but populated) - negotiated_protocol: Option, + negotiated_protocol: Option, // Callback on the fd as soon as the connection has been established #[cfg(unix)] @@ -166,10 +165,12 @@ impl GatewayClient { } #[cfg(not(target_arch = "wasm32"))] + #[allow(clippy::unreachable)] async fn _close_connection(&mut self) -> Result<(), GatewayClientError> { match std::mem::replace(&mut self.connection, SocketState::NotConnected) { SocketState::Available(mut socket) => Ok((*socket).close(None).await?), SocketState::PartiallyDelegated(_) => { + // SAFETY: this is only called after the caller has already recovered the connection unreachable!("this branch should have never been reached!") } _ => Ok(()), // no need to do anything in those cases @@ -177,6 +178,7 @@ impl GatewayClient { } #[cfg(target_arch = "wasm32")] + #[allow(clippy::unreachable)] async fn _close_connection(&mut self) -> Result<(), GatewayClientError> { match std::mem::replace(&mut self.connection, SocketState::NotConnected) { SocketState::Available(socket) => { @@ -184,6 +186,7 @@ impl GatewayClient { Ok(()) } SocketState::PartiallyDelegated(_) => { + // SAFETY: this is only called after the caller has already recovered the connection unreachable!("this branch should have never been reached!") } _ => Ok(()), // no need to do anything in those cases @@ -458,43 +461,16 @@ impl GatewayClient { } } - fn check_gateway_protocol( - &self, - gateway_protocol: Option, - ) -> Result<(), GatewayClientError> { - debug!("gateway protocol: {gateway_protocol:?}, ours: {CURRENT_PROTOCOL_VERSION}"); - - // right now there are no failure cases here, but this might change in the future - match gateway_protocol { - None => { - warn!("the gateway we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0"); - // note: in +1.2.0 we will have to return a hard error here - Ok(()) - } - Some(v) if v > CURRENT_PROTOCOL_VERSION => { - let err = GatewayClientError::IncompatibleProtocol { - gateway: Some(v), - current: CURRENT_PROTOCOL_VERSION, - }; - error!("{err}"); - Err(err) - } - - Some(_) => { - debug!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!"); - Ok(()) - } - } - } - async fn register( &mut self, - derive_aes256_gcm_siv_key: bool, + supported_gateway_protocol: Option, ) -> Result<(), GatewayClientError> { if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } + let derive_aes256_gcm_siv_key = supported_gateway_protocol.supports_aes256_gcm_siv(); + debug_assert!(self.connection.is_available()); log::debug!( "registering with gateway. using legacy key derivation: {}", @@ -505,14 +481,13 @@ impl GatewayClient { // and putting it into the GatewayClient struct would be a hassle let mut rng = OsRng; - let shared_key = match &mut self.connection { + let handshake_result = match &mut self.connection { SocketState::Available(ws_stream) => client_handshake( &mut rng, ws_stream, self.local_identity.as_ref(), self.gateway_identity, - self.cfg.bandwidth.require_tickets, - derive_aes256_gcm_siv_key, + supported_gateway_protocol, #[cfg(not(target_arch = "wasm32"))] self.shutdown_token.clone(), ) @@ -521,26 +496,31 @@ impl GatewayClient { _ => return Err(GatewayClientError::ConnectionInInvalidState), }?; - let (authentication_status, gateway_protocol) = match self.read_control_response().await? { + let authentication_status = match self.read_control_response().await? { ServerResponse::Register { - protocol_version, status, - } => (status, protocol_version), + upgrade_mode, + .. + } => { + if upgrade_mode { + warn!("the system is currently undergoing an upgrade. some of its functionalities might be unstable") + } + status + } ServerResponse::Error { message } => { return Err(GatewayClientError::GatewayError(message)) } other => return Err(GatewayClientError::UnexpectedResponse { name: other.name() }), }; - self.check_gateway_protocol(gateway_protocol)?; self.authenticated = authentication_status; if self.authenticated { - self.shared_key = Some(Arc::new(shared_key)); + self.shared_key = Some(Arc::new(handshake_result.derived_key)); } // populate the negotiated protocol for future uses - self.negotiated_protocol = gateway_protocol; + self.negotiated_protocol = Some(handshake_result.negotiated_protocol); Ok(()) } @@ -623,13 +603,24 @@ impl GatewayClient { protocol_version, status, bandwidth_remaining, + upgrade_mode, } => { - self.check_gateway_protocol(protocol_version)?; + if protocol_version.is_future_version() { + // SAFETY: future version is always defined + #[allow(clippy::unwrap_used)] + let version = protocol_version.unwrap(); + error!("the gateway insists on using v{version} protocol which is not supported by this client"); + return Err(GatewayClientError::AuthenticationFailure); + } self.authenticated = status; - self.bandwidth.update_and_maybe_log(bandwidth_remaining); + self.bandwidth + .update_and_maybe_log(bandwidth_remaining, upgrade_mode); self.negotiated_protocol = protocol_version; log::debug!("authenticated: {status}, bandwidth remaining: {bandwidth_remaining}"); + if upgrade_mode { + warn!("the system is currently undergoing an upgrade. some of its functionalities might be unstable") + } Ok(()) } @@ -650,7 +641,7 @@ impl GatewayClient { .public_key() .derive_destination_address(); - let msg = ClientControlRequest::new_authenticate( + let msg = ClientControlRequest::new_legacy_authenticate( self_address, shared_key, self.cfg.bandwidth.require_tickets, @@ -659,25 +650,40 @@ impl GatewayClient { .await } - async fn authenticate_v2(&mut self) -> Result<(), GatewayClientError> { + async fn authenticate_v2( + &mut self, + requested_protocol_version: GatewayProtocolVersion, + ) -> Result<(), GatewayClientError> { debug!("using v2 authentication"); let Some(shared_key) = self.shared_key.as_ref() else { return Err(GatewayClientError::NoSharedKeyAvailable); }; - let msg = ClientControlRequest::new_authenticate_v2(shared_key, &self.local_identity)?; + let msg = ClientControlRequest::new_authenticate_v2( + shared_key, + &self.local_identity, + requested_protocol_version, + )?; self.send_authenticate_request_and_handle_response(msg) .await } - async fn authenticate(&mut self, use_v2: bool) -> Result<(), GatewayClientError> { + async fn authenticate( + &mut self, + supported_gateway_protocol: Option, + ) -> Result<(), GatewayClientError> { if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } debug!("authenticating with gateway"); - if use_v2 { - self.authenticate_v2().await + if supported_gateway_protocol.supports_authenticate_v2() { + // use the highest possible protocol version the gateway has announced support for + + // SAFETY: if announced protocol supports auth v2, it means it's properly set + #[allow(clippy::unwrap_used)] + self.authenticate_v2(supported_gateway_protocol.unwrap()) + .await } else { self.authenticate_v1().await } @@ -708,9 +714,12 @@ impl GatewayClient { } }; + debug!("supported gateway protocol: {gw_protocol:?}"); + let supports_aes_gcm_siv = gw_protocol.supports_aes256_gcm_siv(); let supports_auth_v2 = gw_protocol.supports_authenticate_v2(); let supports_key_rotation_info = gw_protocol.supports_key_rotation_packet(); + let supports_upgrade_mode = gw_protocol.supports_upgrade_mode(); if !supports_aes_gcm_siv { warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV"); @@ -721,6 +730,16 @@ impl GatewayClient { if !supports_key_rotation_info { warn!("this gateway is on an old version that doesn't support key rotation packets") } + if !supports_upgrade_mode { + warn!("this gateway is on an old version that doesn't support upgrade mode") + } + + let gw_protocol = if gw_protocol.is_future_version() { + warn!("we're running outdated software as gateway is announcing protocol {gw_protocol:?} whilst we're using {}. we're going to attempt to downgrade", GatewayProtocolVersion::CURRENT); + Some(GatewayProtocolVersion::CURRENT) + } else { + gw_protocol + }; if self.authenticated { debug!("Already authenticated"); @@ -735,10 +754,11 @@ impl GatewayClient { } if self.shared_key.is_some() { - self.authenticate(supports_auth_v2).await?; + self.authenticate(gw_protocol).await?; if self.authenticated { // if we are authenticated it means we MUST have an associated shared_key + #[allow(clippy::unwrap_used)] let shared_key = self.shared_key.as_ref().unwrap(); let requires_key_upgrade = shared_key.is_legacy() && supports_aes_gcm_siv; @@ -751,9 +771,10 @@ impl GatewayClient { Err(GatewayClientError::AuthenticationFailure) } } else { - self.register(supports_aes_gcm_siv).await?; + self.register(gw_protocol).await?; // if registration didn't return an error, we MUST have an associated shared key + #[allow(clippy::unwrap_used)] let shared_key = self.shared_key.as_ref().unwrap(); // we're always registering with the highest supported protocol, @@ -783,51 +804,81 @@ impl GatewayClient { } } - async fn claim_ecash_bandwidth( + async fn wait_for_bandwidth_response( &mut self, - credential: CredentialSpendingData, - ) -> Result<(), GatewayClientError> { - let msg = ClientControlRequest::new_enc_ecash_credential( - credential, - self.shared_key.as_ref().unwrap(), - )?; - let bandwidth_remaining = match self + msg: ClientControlRequest, + ) -> Result { + let response = match self .send_websocket_message_with_non_send_response(msg) .await? { - ServerResponse::Bandwidth { available_total } => Ok(available_total), + ServerResponse::Bandwidth(response) => { + if response.upgrade_mode { + info!("the system is currently undergoing an upgrade. our bandwidth shouldn't have been metered") + } + Ok(response) + } ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), ServerResponse::TypedError { error } => { Err(GatewayClientError::TypedGatewayError(error)) } other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }), }?; + Ok(response) + } + + async fn claim_ecash_bandwidth( + &mut self, + credential: CredentialSpendingData, + ) -> Result<(), GatewayClientError> { + // SAFETY: claiming ecash bandwidth is called as part of `claim_bandwidth` which + // ensures the shared key is defined + #[allow(clippy::unwrap_used)] + let msg = ClientControlRequest::new_enc_ecash_credential( + credential, + self.shared_key.as_ref().unwrap(), + )?; + let response = self.wait_for_bandwidth_response(msg).await?; // TODO: create tracing span info!("managed to claim ecash bandwidth"); - self.bandwidth.update_and_log(bandwidth_remaining); + self.bandwidth + .update_and_log(response.available_total, response.upgrade_mode); + + Ok(()) + } + + pub async fn send_upgrade_mode_jwt(&mut self, token: String) -> Result<(), GatewayClientError> { + let msg = ClientControlRequest::new_upgrade_mode_jwt(token); + let response = self.wait_for_bandwidth_response(msg).await?; + + // if gateway rejected our jwt, we would have returned an error + info!("gateway has accepted our jwt"); + if !response.upgrade_mode { + error!("but we're not in upgrade mode - something is wrong!"); + return Err(GatewayClientError::UnexpectedUpgradeModeState); + } + + self.bandwidth + .update_and_log(response.available_total, response.upgrade_mode); Ok(()) } async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> { let msg = ClientControlRequest::ClaimFreeTestnetBandwidth; - let bandwidth_remaining = match self - .send_websocket_message_with_non_send_response(msg) - .await? - { - ServerResponse::Bandwidth { available_total } => Ok(available_total), - ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), - other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }), - }?; + let response = self.wait_for_bandwidth_response(msg).await?; info!("managed to claim testnet bandwidth"); - self.bandwidth.update_and_log(bandwidth_remaining); + self.bandwidth + .update_and_log(response.available_total, response.upgrade_mode); Ok(()) } fn unchecked_bandwidth_controller(&self) -> &BandwidthController { + // this is an unchecked method + #[allow(clippy::unwrap_used)] self.bandwidth_controller.as_ref().unwrap() } @@ -919,6 +970,7 @@ impl GatewayClient { BinaryRequest::ForwardSphinx { packet } }; + #[allow(clippy::expect_used)] req.into_ws_message( self.shared_key .as_ref() @@ -1025,6 +1077,8 @@ impl GatewayClient { self.send_with_reconnection_on_failure(msg).await } + // SAFETY: this method is only called when the connection is in `PartiallyDelegated` state + #[allow(clippy::unreachable)] async fn recover_socket_connection(&mut self) -> Result<(), GatewayClientError> { if self.connection.is_available() { return Ok(()); @@ -1054,6 +1108,7 @@ impl GatewayClient { return Err(GatewayClientError::ConnectionInInvalidState); } + #[allow(clippy::expect_used)] let partially_delegated = match std::mem::replace(&mut self.connection, SocketState::Invalid) { SocketState::Available(conn) => { @@ -1069,7 +1124,13 @@ impl GatewayClient { self.shutdown_token.clone(), ) } - _ => unreachable!(), + other => { + error!( + "attempted to start mixnet listener whilst the connection is in {} state!", + other.name() + ); + return Err(GatewayClientError::ConnectionInInvalidState); + } }; self.connection = SocketState::PartiallyDelegated(partially_delegated); @@ -1082,8 +1143,7 @@ impl GatewayClient { } // if we're reconnecting, because we lost connection, we need to re-authenticate the connection - self.authenticate(self.negotiated_protocol.supports_authenticate_v2()) - .await?; + self.authenticate(self.negotiated_protocol).await?; // this call is NON-blocking self.start_listening_for_mixnet_messages()?; diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index eaea37c586..9f97b7c7f5 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -128,6 +128,9 @@ pub enum GatewayClientError { "this operation couldn't be completed as the program is in the process of shutting down" )] ShutdownInProgress, + + #[error("the system is an unexpected upgrade mode state")] + UnexpectedUpgradeModeState, } impl From for GatewayClientError { diff --git a/common/client-libs/gateway-client/src/packet_router.rs b/common/client-libs/gateway-client/src/packet_router.rs index 7fb863947f..93019564b9 100644 --- a/common/client-libs/gateway-client/src/packet_router.rs +++ b/common/client-libs/gateway-client/src/packet_router.rs @@ -35,6 +35,7 @@ impl PacketRouter { } } + #[allow(clippy::panic)] pub fn route_mixnet_messages( &self, received_messages: Vec>, @@ -54,6 +55,7 @@ impl PacketRouter { Ok(()) } + #[allow(clippy::panic)] pub fn route_acks(&self, received_acks: Vec>) -> Result<(), GatewayClientError> { if let Err(err) = self.ack_sender.unbounded_send(received_acks) { // check if the failure is due to the shutdown being in progress and thus the receiver channel diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index 4f3009e389..151ef4842e 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -10,7 +10,9 @@ use futures::channel::oneshot; use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; use nym_gateway_requests::shared_key::SharedGatewayKey; -use nym_gateway_requests::{SensitiveServerResponse, ServerResponse, SimpleGatewayRequestsError}; +use nym_gateway_requests::{ + SendResponse, SensitiveServerResponse, ServerResponse, SimpleGatewayRequestsError, +}; use nym_task::ShutdownToken; use si_scale::helpers::bibytes2; use std::os::raw::c_int as RawFd; @@ -154,11 +156,12 @@ impl PartiallyDelegatedRouter { fn handle_text_message(&self, text: String) -> Result<(), GatewayClientError> { // if we fail to deserialise the response, return a hard error. we can't handle garbage match ServerResponse::try_from(text).map_err(|_| GatewayClientError::MalformedResponse)? { - ServerResponse::Send { + ServerResponse::Send(SendResponse { remaining_bandwidth, - } => { + upgrade_mode, + }) => { self.client_bandwidth - .update_and_maybe_log(remaining_bandwidth); + .update_and_maybe_log(remaining_bandwidth, upgrade_mode); Ok(()) } ServerResponse::Error { message } => { @@ -174,7 +177,7 @@ impl PartiallyDelegatedRouter { let available_bi2 = bibytes2(available as f64); let required_bi2 = bibytes2(required as f64); warn!("run out of bandwidth when attempting to send the message! we got {available_bi2} available, but needed at least {required_bi2} to send the previous message"); - self.client_bandwidth.update_and_log(available); + self.client_bandwidth.update_and_log(available, None); // UNIMPLEMENTED: we should stop sending messages until we recover bandwidth Ok(()) } @@ -327,6 +330,7 @@ impl PartiallyDelegatedHandle { Ok(self.sink_half.send_all(&mut send_stream).await?) } + #[allow(clippy::panic)] pub(crate) async fn merge(self) -> Result { let (mut stream_receiver, notify) = self.delegated_stream; @@ -355,8 +359,10 @@ impl PartiallyDelegatedHandle { // in receive_res .map_err(|_| GatewayClientError::ConnectionAbruptlyClosed)?; let stream = stream_results?; + // the error is thrown when trying to reunite sink and stream that did not originate // from the same split which is impossible to happen here + #[allow(clippy::unwrap_used)] Ok(self.sink_half.reunite(stream).unwrap()) } } @@ -387,4 +393,13 @@ impl SocketState { SocketState::Available(_) | SocketState::PartiallyDelegated(_) ) } + + pub(crate) fn name(&self) -> &'static str { + match self { + SocketState::Available(_) => "available", + SocketState::PartiallyDelegated(_) => "partially delegated", + SocketState::NotConnected => "not connected", + SocketState::Invalid => "invalid", + } + } } diff --git a/common/credential-proxy/src/error.rs b/common/credential-proxy/src/error.rs index d6e3e8d28f..86219fc5bd 100644 --- a/common/credential-proxy/src/error.rs +++ b/common/credential-proxy/src/error.rs @@ -1,6 +1,7 @@ // Copyright 2025 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_crypto::asymmetric::ed25519; use nym_ecash_signer_check::SignerCheckError; use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nym_api::{EpochId, error::NymAPIError}; @@ -174,8 +175,18 @@ pub enum CredentialProxyError { )] AttestationCheckUrlNotSet, - #[error("the provided attestation check url is malformed: {source}")] + #[error("the provided attester public key is malformed: {source}")] MalformedAttestationCheckUrl { source: url::ParseError }, + + #[error( + "the attester public key has not been provided through either the CLI nor the default .env config" + )] + AttesterPublicKeyNotSet, + + #[error("the provided attester public key is malformed: {source}")] + MalformedAttesterPublicKey { + source: ed25519::Ed25519RecoveryError, + }, } impl From for CredentialProxyError { diff --git a/common/credential-verification/Cargo.toml b/common/credential-verification/Cargo.toml index e85589eaae..06c45efdc5 100644 --- a/common/credential-verification/Cargo.toml +++ b/common/credential-verification/Cargo.toml @@ -17,7 +17,6 @@ cosmwasm-std = { workspace = true } cw-utils = { workspace = true } dyn-clone = { workspace = true } futures = { workspace = true } -rand = { workspace = true } si-scale = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } @@ -27,8 +26,10 @@ tracing = { workspace = true } nym-api-requests = { path = "../../nym-api/nym-api-requests" } nym-credentials = { path = "../credentials" } nym-credentials-interface = { path = "../credentials-interface" } +nym-crypto = { path = "../crypto", features = ["asymmetric"] } nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } nym-gateway-requests = { path = "../gateway-requests" } nym-gateway-storage = { path = "../gateway-storage" } nym-task = { path = "../task" } nym-validator-client = { path = "../client-libs/validator-client" } +nym-upgrade-mode-check = { path = "../upgrade-mode-check" } diff --git a/common/credential-verification/src/bandwidth_storage_manager.rs b/common/credential-verification/src/bandwidth_storage_manager.rs index 7286229e68..9a78b9f95e 100644 --- a/common/credential-verification/src/bandwidth_storage_manager.rs +++ b/common/credential-verification/src/bandwidth_storage_manager.rs @@ -6,7 +6,6 @@ use crate::ClientBandwidth; use crate::error::*; use nym_credentials::ecash::utils::ecash_today; use nym_credentials_interface::Bandwidth; -use nym_gateway_requests::ServerResponse; use nym_gateway_storage::traits::BandwidthGatewayStorage; use si_scale::helpers::bibytes2; use time::OffsetDateTime; @@ -66,7 +65,7 @@ impl BandwidthStorageManager { Ok(()) } - pub async fn handle_claim_testnet_bandwidth(&mut self) -> Result { + pub async fn handle_claim_testnet_bandwidth(&mut self) -> Result { debug!("handling testnet bandwidth request"); if self.only_coconut_credentials { @@ -76,8 +75,7 @@ impl BandwidthStorageManager { self.increase_bandwidth(FREE_TESTNET_BANDWIDTH_VALUE, ecash_today()) .await?; let available_total = self.client_bandwidth.available().await; - - Ok(ServerResponse::Bandwidth { available_total }) + Ok(available_total) } #[instrument(skip_all)] @@ -96,7 +94,7 @@ impl BandwidthStorageManager { let available_bi2 = bibytes2(available_bandwidth as f64); let required_bi2 = bibytes2(required_bandwidth as f64); - debug!(available = available_bi2, required = required_bi2); + trace!(available = available_bi2, required = required_bi2); self.consume_bandwidth(required_bandwidth).await?; let remaining_bandwidth = self.client_bandwidth.available().await; diff --git a/common/credential-verification/src/error.rs b/common/credential-verification/src/error.rs index a00ce0e268..23fb47d8c0 100644 --- a/common/credential-verification/src/error.rs +++ b/common/credential-verification/src/error.rs @@ -47,6 +47,25 @@ pub enum Error { UnknownTicketType(#[from] nym_credentials_interface::UnknownTicketType), } +impl Error { + pub fn is_out_of_bandwidth(&self) -> bool { + matches!(self, Error::OutOfBandwidth { .. }) + } +} + +pub trait OutOfBandwidthResultExt { + fn is_out_of_bandwidth(&self) -> bool; +} + +impl OutOfBandwidthResultExt for Result { + fn is_out_of_bandwidth(&self) -> bool { + match &self { + Ok(_) => false, + Err(err) => err.is_out_of_bandwidth(), + } + } +} + impl From for Error { fn from(err: EcashTicketError) -> Self { // don't expose storage issue details to the user diff --git a/common/credential-verification/src/lib.rs b/common/credential-verification/src/lib.rs index f306867d98..3f2b77f840 100644 --- a/common/credential-verification/src/lib.rs +++ b/common/credential-verification/src/lib.rs @@ -18,6 +18,7 @@ pub mod bandwidth_storage_manager; mod client_bandwidth; pub mod ecash; pub mod error; +pub mod upgrade_mode; pub struct CredentialVerifier { credential: CredentialSpendingRequest, diff --git a/common/credential-verification/src/upgrade_mode.rs b/common/credential-verification/src/upgrade_mode.rs new file mode 100644 index 0000000000..81385bece1 --- /dev/null +++ b/common/credential-verification/src/upgrade_mode.rs @@ -0,0 +1,284 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use nym_crypto::asymmetric::ed25519; +use nym_upgrade_mode_check::{ + CREDENTIAL_PROXY_JWT_ISSUER, UpgradeModeAttestation, validate_upgrade_mode_jwt, +}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; +use std::time::Duration; +use thiserror::Error; +use time::OffsetDateTime; +use tokio::sync::{Notify, RwLock}; +use tracing::{debug, error}; + +#[derive(Debug, Error)] +pub enum UpgradeModeEnableError { + #[error("too soon to perform another upgrade mode attestation check")] + TooManyRecheckRequests, + + #[error("provided upgrade mode JWT is invalid: {0}")] + InvalidUpgradeModeJWT(#[from] nym_upgrade_mode_check::UpgradeModeCheckError), + + #[error("the upgrade mode attestation does not appear to have been published")] + AttestationNotPublished, + + #[error("the provided upgrade mode attestation is different from the published one")] + MismatchedUpgradeModeAttestation, +} + +// the idea behind this is as follows: +// it's been relatively a long time since the watcher last performed its checks (since it's in 'regular' mode) +// and some client has just sent a JWT. we have to retrieve most recent information in case upgrade mode +// has just been enabled, and we haven't learned about it yet +#[derive(Clone)] +pub struct UpgradeModeCheckRequestSender(Option>); + +impl UpgradeModeCheckRequestSender { + pub fn new(sender: UnboundedSender) -> Self { + UpgradeModeCheckRequestSender(Some(sender)) + } + + pub fn new_empty() -> Self { + Self(None) + } + + pub(crate) fn send_request(&self, on_done: Arc) { + let Some(ref inner) = self.0 else { + // make sure the caller gets notified so it doesn't wait forever + on_done.notify_waiters(); + return; + }; + + if let Err(not_sent) = inner.unbounded_send(CheckRequest { on_done }) { + debug!("failed to send upgrade mode check request - {not_sent}"); + // make sure the caller gets notified so it doesn't wait forever + not_sent.into_inner().on_done.notify_waiters(); + } + } +} + +pub type UpgradeModeCheckRequestReceiver = UnboundedReceiver; + +pub struct CheckRequest { + on_done: Arc, +} + +impl CheckRequest { + pub fn finalize(self) { + self.on_done.notify_waiters(); + } +} + +#[derive(Clone, Copy)] +pub struct UpgradeModeCheckConfig { + /// The minimum duration since the last explicit check to allow creation of separate request. + pub min_staleness_recheck: Duration, +} + +/// Full upgrade mode information, that apart from boolean flag indicating the state +/// and the attestation information, includes channel connection to relevant +/// attestation watcher to request state rechecks +#[derive(Clone)] +pub struct UpgradeModeDetails { + pub(crate) config: UpgradeModeCheckConfig, + pub(crate) request_checker: UpgradeModeCheckRequestSender, + pub(crate) state: UpgradeModeState, +} + +impl UpgradeModeDetails { + pub fn new( + config: UpgradeModeCheckConfig, + request_checker: UpgradeModeCheckRequestSender, + state: UpgradeModeState, + ) -> Self { + UpgradeModeDetails { + config, + request_checker, + state, + } + } + + pub fn enabled(&self) -> bool { + self.state.upgrade_mode_enabled() + } + + fn since_last_query(&self) -> Duration { + self.state.since_last_query() + } + + pub fn can_request_recheck(&self) -> bool { + self.since_last_query() > self.config.min_staleness_recheck + } + + // explicitly request state update. this is only called when upgrade mode is NOT enabled, + // and client has sent a JWT instead of ticket + async fn request_recheck(&self) -> bool { + // send request + let on_done = Arc::new(Notify::new()); + self.request_checker.send_request(on_done.clone()); + + // wait for response - note, if we fail to send, notification will be sent regardless, + // so that we wouldn't get stuck in here + on_done.notified().await; + + // check the state again + self.enabled() + } + + pub async fn try_enable_via_received_jwt( + &self, + token: String, + ) -> Result<(), UpgradeModeEnableError> { + // see if it's viable to perform another expedited check + if !self.can_request_recheck() { + return Err(UpgradeModeEnableError::TooManyRecheckRequests); + } + + // first validate whether the received JWT is even valid + let attestation = validate_upgrade_mode_jwt(&token, Some(CREDENTIAL_PROXY_JWT_ISSUER))?; + + // send request to revalidate internal state + // this will, among other things, pull fresh attestation from the configured endpoint + // and also verify required signatures (and pubkeys) + self.request_recheck().await; + + // not strictly necessary, but check if provided attestation actually matches the one retrieved + // (if any) + let Some(retrieved_attestation) = self.state.attestation().await else { + return Err(UpgradeModeEnableError::AttestationNotPublished); + }; + if retrieved_attestation != attestation { + return Err(UpgradeModeEnableError::MismatchedUpgradeModeAttestation); + } + + // note: if attestation has been returned, it means we're definitely in upgrade mode + // (otherwise it wouldn't have existed in the state) + + Ok(()) + } +} + +/// Detailed upgrade mode information, that apart from boolean flag, +/// also includes, if applicable, the associated attestation +#[derive(Clone)] +pub struct UpgradeModeState { + inner: Arc, +} + +/// Just a shareable flag to indicate whether upgrade mode is enabled or disabled +#[derive(Clone, Default)] +pub struct UpgradeModeStatus(Arc); + +impl UpgradeModeStatus { + pub fn enabled(&self) -> bool { + self.0.load(Ordering::Acquire) + } + + pub fn enable(&self) { + self.0.store(true, Ordering::Relaxed); + } + + pub fn disable(&self) { + self.0.store(false, Ordering::Release); + } +} + +impl UpgradeModeState { + pub fn new(attester_public_key: ed25519::PublicKey) -> UpgradeModeState { + UpgradeModeState { + inner: Arc::new(UpgradeModeStateInner { + expected_attester_public_key: attester_public_key, + expected_attestation: RwLock::new(None), + last_queried_ts: AtomicI64::new(OffsetDateTime::UNIX_EPOCH.unix_timestamp()), + status: UpgradeModeStatus(Arc::new(AtomicBool::new(false))), + }), + } + } + + pub async fn attestation(&self) -> Option { + self.inner.expected_attestation.read().await.clone() + } + + pub async fn try_set_expected_attestation( + &self, + expected_attestation: Option, + ) { + // make sure to only enable upgrade mode flag AFTER we have written the expected value + // (or still hold the exclusive lock as in this instance) + let mut guard = self.inner.expected_attestation.write().await; + + // ensure that the attestation had been signed with the expected key + if let Some(attestation) = expected_attestation.as_ref() { + if attestation.content.attester_public_key != self.inner.expected_attester_public_key { + self.update_last_queried(OffsetDateTime::now_utc()); + return; + } + + self.enable_upgrade_mode() + } else { + self.disable_upgrade_mode() + } + + self.update_last_queried(OffsetDateTime::now_utc()); + *guard = expected_attestation; + } + + pub fn upgrade_mode_status(&self) -> UpgradeModeStatus { + self.inner.status.clone() + } + + pub fn upgrade_mode_enabled(&self) -> bool { + self.inner.status.enabled() + } + + pub fn enable_upgrade_mode(&self) { + self.inner.status.enable() + } + + pub fn disable_upgrade_mode(&self) { + self.inner.status.disable() + } + + pub fn last_queried(&self) -> OffsetDateTime { + // SAFETY: the stored value here is always a valid unix timestamp + #[allow(clippy::unwrap_used)] + OffsetDateTime::from_unix_timestamp(self.inner.last_queried_ts.load(Ordering::Acquire)) + .unwrap() + } + + pub fn update_last_queried(&self, queried_at: OffsetDateTime) { + self.inner + .last_queried_ts + .store(queried_at.unix_timestamp(), Ordering::Release); + } + + pub fn since_last_query(&self) -> Duration { + (OffsetDateTime::now_utc() - self.last_queried()) + .try_into() + .unwrap_or_else(|_| { + error!("somehow our last query for upgrade mode was in the future!"); + Duration::ZERO + }) + } +} + +struct UpgradeModeStateInner { + /// Expected public key of the entity issuing upgrade mode attestations. + expected_attester_public_key: ed25519::PublicKey, + + /// Contents of the published upgrade mode attestation, as queried by this node + expected_attestation: RwLock>, + + /// timestamp indicating last time this node has queried for the current upgrade mode attestation + /// it is used to determine if an additional expedited query should be made in case client sends a JWT + /// whilst this node is not aware of the upgrade mode + last_queried_ts: AtomicI64, + + /// flag indicating whether upgrade mode is currently enabled. this is to perform cheap checks + /// that avoid having to acquire the lock + // (and dealing with the async consequences of that) + status: UpgradeModeStatus, +} diff --git a/common/credentials-interface/Cargo.toml b/common/credentials-interface/Cargo.toml index d6d4ce842b..57a88ea603 100644 --- a/common/credentials-interface/Cargo.toml +++ b/common/credentials-interface/Cargo.toml @@ -23,3 +23,5 @@ rand = { workspace = true } nym-compact-ecash = { path = "../nym_offline_compact_ecash" } nym-ecash-time = { path = "../ecash-time" } nym-network-defaults = { path = "../network-defaults" } +nym-upgrade-mode-check = { path = "../upgrade-mode-check" } + diff --git a/common/credentials-interface/src/lib.rs b/common/credentials-interface/src/lib.rs index 91e08ee770..807016792b 100644 --- a/common/credentials-interface/src/lib.rs +++ b/common/credentials-interface/src/lib.rs @@ -30,6 +30,35 @@ pub use nym_compact_ecash::{ }; pub use nym_ecash_time::{EcashTime, ecash_today}; pub use nym_network_defaults::TicketTypeRepr; +use nym_network_defaults::TicketTypeRepr::V1MixnetEntry; + +/// Default bandwidth amount under which [mixnet] clients will attempt to send additional zk-nyms +/// to increase their allowance. +// currently defined as 20% of entry ticket value +// clients are, of course, free to override this value +pub const DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD: i64 = + (V1MixnetEntry.bandwidth_value() / 5) as i64; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum BandwidthCredential { + ZkNym(Box), + UpgradeModeJWT { token: String }, +} + +impl BandwidthCredential { + pub fn into_zk_nym(self) -> Option> { + match self { + BandwidthCredential::ZkNym(credential) => Some(credential), + _ => None, + } + } +} + +impl From for BandwidthCredential { + fn from(credential: CredentialSpendingData) -> Self { + Self::ZkNym(Box::new(credential)) + } +} #[derive(Debug, Clone)] pub struct CredentialSigningData { diff --git a/common/gateway-requests/Cargo.toml b/common/gateway-requests/Cargo.toml index 83042ead14..65a5704fdc 100644 --- a/common/gateway-requests/Cargo.toml +++ b/common/gateway-requests/Cargo.toml @@ -51,3 +51,6 @@ anyhow = { workspace = true } nym-compact-ecash = { path = "../nym_offline_compact_ecash" } # we need specific imports in tests nym-test-utils = { path = "../test-utils" } tokio = { workspace = true, features = ["full"] } + +[lints] +workspace = true \ No newline at end of file diff --git a/common/gateway-requests/src/lib.rs b/common/gateway-requests/src/lib.rs index bdb9a30a0f..a0f44b20fc 100644 --- a/common/gateway-requests/src/lib.rs +++ b/common/gateway-requests/src/lib.rs @@ -19,7 +19,9 @@ pub use shared_key::{ SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey, }; -pub const CURRENT_PROTOCOL_VERSION: u8 = EMBEDDED_KEY_ROTATION_INFO_VERSION; +pub type GatewayProtocolVersion = u8; + +pub const CURRENT_PROTOCOL_VERSION: GatewayProtocolVersion = UPGRADE_MODE_VERSION; /// Defines the current version of the communication protocol between gateway and clients. /// It has to be incremented for any breaking change. @@ -29,35 +31,73 @@ pub const CURRENT_PROTOCOL_VERSION: u8 = EMBEDDED_KEY_ROTATION_INFO_VERSION; // 3 - change to AES-GCM-SIV and non-zero IVs // 4 - introduction of v2 authentication protocol to prevent reply attacks // 5 - add key rotation information to the serialised mix packet -pub const INITIAL_PROTOCOL_VERSION: u8 = 1; -pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: u8 = 2; -pub const AES_GCM_SIV_PROTOCOL_VERSION: u8 = 3; -pub const AUTHENTICATE_V2_PROTOCOL_VERSION: u8 = 4; -pub const EMBEDDED_KEY_ROTATION_INFO_VERSION: u8 = 5; +// 6 - support for 'upgrade mode' +pub const INITIAL_PROTOCOL_VERSION: GatewayProtocolVersion = 1; +pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: GatewayProtocolVersion = 2; +pub const AES_GCM_SIV_PROTOCOL_VERSION: GatewayProtocolVersion = 3; +pub const AUTHENTICATE_V2_PROTOCOL_VERSION: GatewayProtocolVersion = 4; +pub const EMBEDDED_KEY_ROTATION_INFO_VERSION: GatewayProtocolVersion = 5; +pub const UPGRADE_MODE_VERSION: GatewayProtocolVersion = 6; // TODO: could using `Mac` trait here for OutputSize backfire? // Should hmac itself be exposed, imported and used instead? pub type LegacyGatewayMacSize = ::OutputSize; pub trait GatewayProtocolVersionExt { + const CURRENT: GatewayProtocolVersion = CURRENT_PROTOCOL_VERSION; + fn supports_aes256_gcm_siv(&self) -> bool; fn supports_authenticate_v2(&self) -> bool; fn supports_key_rotation_packet(&self) -> bool; + fn supports_upgrade_mode(&self) -> bool; + fn is_future_version(&self) -> bool; } -impl GatewayProtocolVersionExt for Option { +impl GatewayProtocolVersionExt for Option { fn supports_aes256_gcm_siv(&self) -> bool { - let Some(protocol) = *self else { return false }; - protocol >= AES_GCM_SIV_PROTOCOL_VERSION + let Some(protocol) = self else { return false }; + protocol.supports_aes256_gcm_siv() } fn supports_authenticate_v2(&self) -> bool { - let Some(protocol) = *self else { return false }; - protocol >= AUTHENTICATE_V2_PROTOCOL_VERSION + let Some(protocol) = self else { return false }; + protocol.supports_authenticate_v2() } fn supports_key_rotation_packet(&self) -> bool { - let Some(protocol) = *self else { return false }; - protocol >= EMBEDDED_KEY_ROTATION_INFO_VERSION + let Some(protocol) = self else { return false }; + protocol.supports_key_rotation_packet() + } + + fn supports_upgrade_mode(&self) -> bool { + let Some(protocol) = self else { return false }; + protocol.supports_upgrade_mode() + } + + fn is_future_version(&self) -> bool { + let Some(protocol) = self else { return false }; + protocol.is_future_version() + } +} + +impl GatewayProtocolVersionExt for GatewayProtocolVersion { + fn supports_aes256_gcm_siv(&self) -> bool { + *self >= AES_GCM_SIV_PROTOCOL_VERSION + } + + fn supports_authenticate_v2(&self) -> bool { + *self >= AUTHENTICATE_V2_PROTOCOL_VERSION + } + + fn supports_key_rotation_packet(&self) -> bool { + *self >= EMBEDDED_KEY_ROTATION_INFO_VERSION + } + + fn supports_upgrade_mode(&self) -> bool { + *self >= UPGRADE_MODE_VERSION + } + + fn is_future_version(&self) -> bool { + *self > CURRENT_PROTOCOL_VERSION } } diff --git a/common/gateway-requests/src/registration/handshake/client.rs b/common/gateway-requests/src/registration/handshake/client.rs index 549cddca39..7f2a3ccb19 100644 --- a/common/gateway-requests/src/registration/handshake/client.rs +++ b/common/gateway-requests/src/registration/handshake/client.rs @@ -3,10 +3,12 @@ use crate::registration::handshake::messages::{Finalization, GatewayMaterialExchange}; use crate::registration::handshake::state::State; -use crate::registration::handshake::SharedGatewayKey; +use crate::registration::handshake::HandshakeResult; use crate::registration::handshake::{error::HandshakeError, WsItem}; +use crate::{GatewayProtocolVersionExt, INITIAL_PROTOCOL_VERSION}; use futures::{Sink, Stream}; use rand::{CryptoRng, RngCore}; +use tracing::info; use tungstenite::Message as WsMessage; impl State<'_, S, R> { @@ -25,10 +27,26 @@ impl State<'_, S, R> { // 2. wait for response with remote x25519 pubkey as well as encrypted signature // <- g^y || AES(k, sig(gate_priv, (g^y || g^x)) || MAYBE_NONCE - let mid_res = self + let (mid_res, gateway_protocol) = self .receive_handshake_message::() .await?; + // NEGOTIATE PROTOCOL + if gateway_protocol.is_future_version() { + // SAFETY: future version means it's greater than CURRENT, which is always a `Some` + #[allow(clippy::unwrap_used)] + return Err(HandshakeError::UnsupportedProtocol { + version: gateway_protocol.unwrap(), + }); + } + let gateway_protocol = gateway_protocol.unwrap_or(INITIAL_PROTOCOL_VERSION); + + // that should never happen, but we're fine with that outcome + if Some(gateway_protocol) != self.proposed_protocol_version() { + info!("the gateway insists on protocol version different from the one we suggested. it wants {gateway_protocol} whilst we wanted {:?}, however, we can support it", self.proposed_protocol_version()); + self.set_protocol_version(gateway_protocol); + } + // 3. derive shared keys locally // hkdf::::(g^xy) self.derive_shared_key(&mid_res.ephemeral_dh, maybe_hkdf_salt.as_deref()); @@ -42,14 +60,14 @@ impl State<'_, S, R> { self.send_handshake_data(materials).await?; // 6. wait for remote confirmation of finalizing the handshake - let finalization = self.receive_handshake_message::().await?; + let (finalization, _) = self.receive_handshake_message::().await?; finalization.ensure_success()?; Ok(()) } pub(crate) async fn perform_client_handshake( mut self, - ) -> Result + ) -> Result where S: Stream + Sink + Unpin, R: CryptoRng + RngCore, diff --git a/common/gateway-requests/src/registration/handshake/error.rs b/common/gateway-requests/src/registration/handshake/error.rs index 8cc9cf1c0d..67dc02d009 100644 --- a/common/gateway-requests/src/registration/handshake/error.rs +++ b/common/gateway-requests/src/registration/handshake/error.rs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::shared_key::SharedKeyUsageError; +use crate::GatewayProtocolVersion; +use crate::GatewayProtocolVersionExt; use thiserror::Error; #[derive(Debug, Error)] @@ -34,4 +36,10 @@ pub enum HandshakeError { #[error("timed out waiting for a handshake message")] Timeout, + + #[error("Connection is in an invalid state - please send a bug report")] + ConnectionInInvalidState, + + #[error("the gateway requests protocol version that's not supported by this client. it wants to use v{version} whilst we only understand up to v{}", GatewayProtocolVersion::CURRENT)] + UnsupportedProtocol { version: GatewayProtocolVersion }, } diff --git a/common/gateway-requests/src/registration/handshake/gateway.rs b/common/gateway-requests/src/registration/handshake/gateway.rs index 5fec717c46..cea8fd67de 100644 --- a/common/gateway-requests/src/registration/handshake/gateway.rs +++ b/common/gateway-requests/src/registration/handshake/gateway.rs @@ -5,9 +5,11 @@ use crate::registration::handshake::messages::{ HandshakeMessage, Initialisation, MaterialExchange, }; use crate::registration::handshake::state::State; -use crate::registration::handshake::SharedGatewayKey; +use crate::registration::handshake::HandshakeResult; use crate::registration::handshake::{error::HandshakeError, WsItem}; +use crate::{GatewayProtocolVersion, GatewayProtocolVersionExt}; use futures::{Sink, Stream}; +use tracing::{debug, warn}; use tungstenite::Message as WsMessage; impl State<'_, S, R> { @@ -18,11 +20,39 @@ impl State<'_, S, R> { where S: Stream + Sink + Unpin, { + // NEGOTIATE PROTOCOL + // old clients were sending protocol version as defined by the following: + /* + fn request_protocol_version(&self) -> u8 { + if self.derive_aes256_gcm_siv_key { + AES_GCM_SIV_PROTOCOL_VERSION + } else if self.expects_credential_usage { + CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION + } else { + INITIAL_PROTOCOL_VERSION + } + } + */ + // meaning the highest possible value they could have sent was `4` (AUTHENTICATE_V2_PROTOCOL_VERSION) + // so if we received anything higher than that, it means they understand negotiation. + // currently not strictly needed as we just blindly accept what they proposed, + // but will be needed in the future. + if self.proposed_protocol_version().is_future_version() { + // this should never happen in a non-malicious client as it should use at most whatever version this gateway has announced + self.set_protocol_version(GatewayProtocolVersion::CURRENT) + } else { + // currently we accept all protocols, i.e. legacy keys, aes128, etc. so we downgrade to whatever + // the client has proposed. this will change in the future + debug!( + "using the protocol version proposed by the client: {:?}", + self.proposed_protocol_version() + ) + } + // 1. receive remote ed25519 pubkey alongside ephemeral x25519 pubkey and maybe a flag indicating non-legacy client // LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_NON_LEGACY let init_message = Initialisation::try_from_bytes(&raw_init_message)?; self.update_remote_identity(init_message.identity); - self.set_aes256_gcm_siv_key_derivation(!init_message.is_legacy()); // 2. derive shared keys locally // hkdf::::(g^xy) @@ -39,7 +69,12 @@ impl State<'_, S, R> { self.send_handshake_data(material).await?; // 4. wait for the remote response with their own encrypted signature - let materials = self.receive_handshake_message::().await?; + let (materials, client_protocol) = + self.receive_handshake_message::().await?; + if client_protocol != self.proposed_protocol_version() { + warn!("the client hasn't accepted our proposed protocol version. we suggested {:?} while it returned {client_protocol:?}", self.proposed_protocol_version()); + // TBD what to do here + } // 5. verify the received signature using the locally derived keys self.verify_remote_key_material(&materials, &init_message.ephemeral_dh)?; @@ -54,7 +89,7 @@ impl State<'_, S, R> { pub(crate) async fn perform_gateway_handshake( mut self, raw_init_message: Vec, - ) -> Result + ) -> Result where S: Stream + Sink + Unpin, { diff --git a/common/gateway-requests/src/registration/handshake/messages.rs b/common/gateway-requests/src/registration/handshake/messages.rs index 10dda7edaf..9958c17ba1 100644 --- a/common/gateway-requests/src/registration/handshake/messages.rs +++ b/common/gateway-requests/src/registration/handshake/messages.rs @@ -24,13 +24,6 @@ pub struct Initialisation { pub initiator_salt: Option>, } -impl Initialisation { - #[cfg(not(target_arch = "wasm32"))] - pub fn is_legacy(&self) -> bool { - self.initiator_salt.is_none() - } -} - #[derive(Debug)] pub struct MaterialExchange { pub signature_ciphertext: Vec, @@ -99,8 +92,9 @@ impl HandshakeMessage for Initialisation { let identity = ed25519::PublicKey::from_bytes(&bytes[..ed25519::PUBLIC_KEY_LENGTH]) .map_err(|_| HandshakeError::MalformedRequest)?; - // this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE + // SAFETY: this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE // which is impossible + #[allow(clippy::unwrap_used)] let ephemeral_dh = x25519::PublicKey::from_bytes(&bytes[ed25519::PUBLIC_KEY_LENGTH..legacy_len]).unwrap(); @@ -194,6 +188,7 @@ impl HandshakeMessage for GatewayMaterialExchange { // this can only fail if the provided bytes have len different from PUBLIC_KEY_SIZE // which is impossible + #[allow(clippy::unwrap_used)] let ephemeral_dh = x25519::PublicKey::from_bytes(&bytes[..x25519::PUBLIC_KEY_SIZE]).unwrap(); let materials = MaterialExchange::try_from_bytes(&bytes[x25519::PUBLIC_KEY_SIZE..])?; diff --git a/common/gateway-requests/src/registration/handshake/mod.rs b/common/gateway-requests/src/registration/handshake/mod.rs index 2daed81b48..8b1972f921 100644 --- a/common/gateway-requests/src/registration/handshake/mod.rs +++ b/common/gateway-requests/src/registration/handshake/mod.rs @@ -3,7 +3,7 @@ use self::error::HandshakeError; use crate::registration::handshake::state::State; -use crate::SharedGatewayKey; +use crate::{GatewayProtocolVersion, SharedGatewayKey}; use futures::future::BoxFuture; use futures::{Sink, Stream}; use nym_crypto::asymmetric::ed25519; @@ -34,24 +34,29 @@ pub const KDF_SALT_LENGTH: usize = 16; // we do not need to worry about that. pub struct GatewayHandshake<'a> { - handshake_future: BoxFuture<'a, Result>, + handshake_future: BoxFuture<'a, Result>, } impl Future for GatewayHandshake<'_> { - type Output = Result; + type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { Pin::new(&mut self.handshake_future).poll(cx) } } +#[derive(Debug, PartialEq)] +pub struct HandshakeResult { + pub negotiated_protocol: GatewayProtocolVersion, + pub derived_key: SharedGatewayKey, +} + pub fn client_handshake<'a, S, R>( rng: &'a mut R, ws_stream: &'a mut S, identity: &'a ed25519::KeyPair, gateway_pubkey: ed25519::PublicKey, - expects_credential_usage: bool, - derive_aes256_gcm_siv_key: bool, + gateway_protocol: Option, #[cfg(not(target_arch = "wasm32"))] shutdown_token: ShutdownToken, ) -> GatewayHandshake<'a> where @@ -63,11 +68,10 @@ where ws_stream, identity, Some(gateway_pubkey), + gateway_protocol, #[cfg(not(target_arch = "wasm32"))] shutdown_token, - ) - .with_credential_usage(expects_credential_usage) - .with_aes256_gcm_siv_key(derive_aes256_gcm_siv_key); + ); GatewayHandshake { handshake_future: Box::pin(state.perform_client_handshake()), @@ -80,13 +84,21 @@ pub fn gateway_handshake<'a, S, R>( ws_stream: &'a mut S, identity: &'a ed25519::KeyPair, received_init_payload: Vec, + requested_client_protocol: Option, shutdown_token: ShutdownToken, ) -> GatewayHandshake<'a> where S: Stream + Sink + Unpin + Send + 'a, R: CryptoRng + RngCore + Send, { - let state = State::new(rng, ws_stream, identity, None, shutdown_token); + let state = State::new( + rng, + ws_stream, + identity, + None, + requested_client_protocol, + shutdown_token, + ); GatewayHandshake { handshake_future: Box::pin(state.perform_gateway_handshake(received_init_payload)), } @@ -113,7 +125,8 @@ DONE(status) #[cfg(test)] mod tests { use super::*; - use crate::ClientControlRequest; + use crate::{ClientControlRequest, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION}; + use anyhow::{bail, Context}; use futures::StreamExt; use nym_test_utils::helpers::u64_seeded_rng; use nym_test_utils::mocks::stream_sink::mock_streams; @@ -121,10 +134,53 @@ mod tests { use tokio::join; use tungstenite::Message; - #[tokio::test] - async fn basic_handshake() -> anyhow::Result<()> { - use anyhow::Context as _; + trait ClientControlRequestExt { + async fn get_handshake_init_data(&mut self) -> anyhow::Result> { + let ClientControlRequest::RegisterHandshakeInitRequest { + protocol_version: _, + data, + } = self.get_control_request().await? + else { + bail!("unexpected ClientControlRequest") + }; + Ok(data) + } + async fn get_control_request(&mut self) -> anyhow::Result; + } + impl ClientControlRequestExt for T + where + T: Stream + Unpin, + { + async fn get_control_request(&mut self) -> anyhow::Result { + let msg = self + .next() + .timeboxed() + .await + .context("timeout")? + .context("no message!")?? + .into_text()? + .parse::()?; + Ok(msg) + } + } + + struct Party { + rng: &'static mut R, + keys: &'static mut ed25519::KeyPair, + socket: &'static mut S, + } + + fn setup() -> ( + Party< + impl CryptoRng + RngCore + Send, + impl Stream + Sink + Unpin, + >, + Party< + impl CryptoRng + RngCore + Send, + impl Stream + Sink + Unpin, + >, + ) { // solve the lifetime issue by just leaking the contents of the boxes // which is perfectly fine in test let client_rng = u64_seeded_rng(42).leak(); @@ -142,51 +198,139 @@ mod tests { let client_ws = client_ws.leak(); let gateway_ws = gateway_ws.leak(); + ( + Party { + rng: client_rng, + keys: client_keys, + socket: client_ws, + }, + Party { + rng: gateway_rng, + keys: gateway_keys, + socket: gateway_ws, + }, + ) + } + + #[tokio::test] + async fn basic_handshake() -> anyhow::Result<()> { + let (client, gateway) = setup(); + let handshake_client = client_handshake( - client_rng, - client_ws, - client_keys, - *gateway_keys.public_key(), - false, - true, + client.rng, + client.socket, + client.keys, + *gateway.keys.public_key(), + Some(CURRENT_PROTOCOL_VERSION), ShutdownToken::default(), ); let client_fut = handshake_client.spawn_timeboxed(); // we need to receive the first message so that it could be propagated to the gateway side of the handshake - let ClientControlRequest::RegisterHandshakeInitRequest { - protocol_version: _, - data, - } = (gateway_ws.next()) - .timeboxed() - .await - .context("timeout")? - .context("no message!")?? - .into_text()? - .parse::()? - else { - panic!("bad message") - }; - - let init_msg = data; + let init_msg = gateway.socket.get_handshake_init_data().await?; let handshake_gateway = gateway_handshake( - gateway_rng, - gateway_ws, - gateway_keys, + gateway.rng, + gateway.socket, + gateway.keys, init_msg, + Some(CURRENT_PROTOCOL_VERSION), ShutdownToken::default(), ); let gateway_fut = handshake_gateway.spawn_timeboxed(); let (client, gateway) = join!(client_fut, gateway_fut); - let client_key = client???; - let gateway_key = gateway???; + let client_res = client???; + let gateway_res = gateway???; // ensure the created keys are the same - assert_eq!(client_key, gateway_key); + assert_eq!(client_res, gateway_res); + assert_eq!(client_res.negotiated_protocol, CURRENT_PROTOCOL_VERSION); + + Ok(()) + } + + #[tokio::test] + async fn protocol_downgrade() -> anyhow::Result<()> { + let (client, gateway) = setup(); + + let handshake_client = client_handshake( + client.rng, + client.socket, + client.keys, + *gateway.keys.public_key(), + Some(CURRENT_PROTOCOL_VERSION + 42), + ShutdownToken::default(), + ); + + let client_fut = handshake_client.spawn_timeboxed(); + // we need to receive the first message so that it could be propagated to the gateway side of the handshake + let init_msg = gateway.socket.get_handshake_init_data().await?; + + let handshake_gateway = gateway_handshake( + gateway.rng, + gateway.socket, + gateway.keys, + init_msg, + Some(CURRENT_PROTOCOL_VERSION + 42), + ShutdownToken::default(), + ); + + let gateway_fut = handshake_gateway.spawn_timeboxed(); + let (client, gateway) = join!(client_fut, gateway_fut); + + let client_res = client???; + let gateway_res = gateway???; + + // ensure the created keys are the same + assert_eq!(client_res, gateway_res); + + // and the protocol got downgraded for both parties + assert_eq!(client_res.negotiated_protocol, CURRENT_PROTOCOL_VERSION); + + Ok(()) + } + + #[tokio::test] + async fn protocol_upgrade() -> anyhow::Result<()> { + let (client, gateway) = setup(); + + let handshake_client = client_handshake( + client.rng, + client.socket, + client.keys, + *gateway.keys.public_key(), + None, + ShutdownToken::default(), + ); + + let client_fut = handshake_client.spawn_timeboxed(); + + // we need to receive the first message so that it could be propagated to the gateway side of the handshake + let init_msg = gateway.socket.get_handshake_init_data().await?; + + let handshake_gateway = gateway_handshake( + gateway.rng, + gateway.socket, + gateway.keys, + init_msg, + None, + ShutdownToken::default(), + ); + + let gateway_fut = handshake_gateway.spawn_timeboxed(); + let (client, gateway) = join!(client_fut, gateway_fut); + + let client_res = client???; + let gateway_res = gateway???; + + // ensure the created keys are the same + assert_eq!(client_res, gateway_res); + + // and the protocol got upgraded to the first known version + assert_eq!(client_res.negotiated_protocol, INITIAL_PROTOCOL_VERSION); Ok(()) } diff --git a/common/gateway-requests/src/registration/handshake/state.rs b/common/gateway-requests/src/registration/handshake/state.rs index 3d7b29e3f9..1e51fda37b 100644 --- a/common/gateway-requests/src/registration/handshake/state.rs +++ b/common/gateway-requests/src/registration/handshake/state.rs @@ -5,11 +5,11 @@ use crate::registration::handshake::error::HandshakeError; use crate::registration::handshake::messages::{ HandshakeMessage, Initialisation, MaterialExchange, }; -use crate::registration::handshake::{SharedGatewayKey, WsItem, KDF_SALT_LENGTH}; +use crate::registration::handshake::{HandshakeResult, SharedGatewayKey, WsItem, KDF_SALT_LENGTH}; use crate::shared_key::SharedKeySize; use crate::{ - types, LegacySharedKeySize, LegacySharedKeys, SharedSymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION, - CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, + types, GatewayProtocolVersion, GatewayProtocolVersionExt, LegacySharedKeySize, + LegacySharedKeys, SharedSymmetricKey, INITIAL_PROTOCOL_VERSION, }; use futures::{Sink, SinkExt, Stream, StreamExt}; use nym_crypto::asymmetric::{ed25519, x25519}; @@ -54,12 +54,11 @@ pub(crate) struct State<'a, S, R> { /// Ideally it would always be known before the handshake was initiated. remote_pubkey: Option, - // this field is really out of place here, however, we need to propagate this information somehow - // in order to establish correct protocol for backwards compatibility reasons - expects_credential_usage: bool, - - /// Specifies whether the end product should be an AES128Ctr + blake3 HMAC keys (legacy) or AES256-GCM-SIV (current) - derive_aes256_gcm_siv_key: bool, + /// Version of the protocol to use during the handshake that also implicitly specifies + /// additional features such as the type of derived shared keys, i.e. + /// AES128Ctr + blake3 HMAC keys (legacy) or AES256-GCM-SIV (current) + /// the above is decided by whether the specified protocol version supports the new variant or not. + protocol_version: Option, // channel to receive shutdown signal #[cfg(not(target_arch = "wasm32"))] @@ -72,6 +71,7 @@ impl<'a, S, R> State<'a, S, R> { ws_stream: &'a mut S, identity: &'a ed25519::KeyPair, remote_pubkey: Option, + protocol_version: Option, #[cfg(not(target_arch = "wasm32"))] shutdown_token: ShutdownToken, ) -> Self where @@ -84,40 +84,31 @@ impl<'a, S, R> State<'a, S, R> { ephemeral_keypair, identity, remote_pubkey, + protocol_version, derived_shared_keys: None, - // later on this should become the default - expects_credential_usage: false, - derive_aes256_gcm_siv_key: false, #[cfg(not(target_arch = "wasm32"))] shutdown_token, } } - pub(crate) fn with_credential_usage(mut self, expects_credential_usage: bool) -> Self { - self.expects_credential_usage = expects_credential_usage; - self - } - - pub(crate) fn with_aes256_gcm_siv_key(mut self, derive_aes256_gcm_siv_key: bool) -> Self { - self.derive_aes256_gcm_siv_key = derive_aes256_gcm_siv_key; - self - } - - #[cfg(not(target_arch = "wasm32"))] - pub(crate) fn set_aes256_gcm_siv_key_derivation(&mut self, derive_aes256_gcm_siv_key: bool) { - self.derive_aes256_gcm_siv_key = derive_aes256_gcm_siv_key; - } - #[cfg(not(target_arch = "wasm32"))] pub(crate) fn local_ephemeral_key(&self) -> &x25519::PublicKey { self.ephemeral_keypair.public_key() } + pub(crate) fn proposed_protocol_version(&self) -> Option { + self.protocol_version + } + + pub(crate) fn set_protocol_version(&mut self, protocol_version: GatewayProtocolVersion) { + self.protocol_version = Some(protocol_version); + } + pub(crate) fn maybe_generate_initiator_salt(&mut self) -> Option> where R: CryptoRng + RngCore, { - if self.derive_aes256_gcm_siv_key { + if self.protocol_version.supports_aes256_gcm_siv() { let mut salt = vec![0u8; KDF_SALT_LENGTH]; self.rng.fill_bytes(&mut salt); Some(salt) @@ -154,13 +145,14 @@ impl<'a, S, R> State<'a, S, R> { .private_key() .diffie_hellman(remote_ephemeral_key); - let key_size = if self.derive_aes256_gcm_siv_key { + let key_size = if self.protocol_version.supports_aes256_gcm_siv() { SharedKeySize::to_usize() } else { LegacySharedKeySize::to_usize() }; - // there is no reason for this to fail as our okm is expected to be only 16 bytes + // SAFETY: there is no reason for this to fail as our okm is expected to be only 16 bytes + #[allow(clippy::expect_used)] let okm = hkdf::extract_then_expand::( initiator_salt, &dh_result, @@ -169,11 +161,14 @@ impl<'a, S, R> State<'a, S, R> { ) .expect("somehow too long okm was provided"); - let shared_key = if self.derive_aes256_gcm_siv_key { + // SAFETY: the okm has been expanded to the length expected by the corresponding keys + let shared_key = if self.protocol_version.supports_aes256_gcm_siv() { + #[allow(clippy::expect_used)] let current_key = SharedSymmetricKey::try_from_bytes(&okm) .expect("okm was expanded to incorrect length!"); SharedGatewayKey::Current(current_key) } else { + #[allow(clippy::expect_used)] let legacy_key = LegacySharedKeys::try_from_bytes(&okm) .expect("okm was expanded to incorrect length!"); SharedGatewayKey::Legacy(legacy_key) @@ -196,7 +191,7 @@ impl<'a, S, R> State<'a, S, R> { .collect(); let signature = self.identity.private_key().sign(plaintext); - let nonce = if self.derive_aes256_gcm_siv_key { + let nonce = if self.protocol_version.supports_aes256_gcm_siv() { let mut rng = thread_rng(); Some(random_nonce::(&mut rng).to_vec()) } else { @@ -204,6 +199,7 @@ impl<'a, S, R> State<'a, S, R> { }; // SAFETY: this function is only called after the local key has already been derived + #[allow(clippy::expect_used)] let signature_ciphertext = self .derived_shared_keys .as_ref() @@ -222,13 +218,14 @@ impl<'a, S, R> State<'a, S, R> { remote_ephemeral_key: &x25519::PublicKey, ) -> Result<(), HandshakeError> { // SAFETY: this function is only called after the local key has already been derived + #[allow(clippy::expect_used)] let derived_shared_key = self .derived_shared_keys .as_ref() .expect("shared key was not derived!"); // if the [client] init message contained non-legacy flag, the associated nonce MUST be present - if self.derive_aes256_gcm_siv_key && remote_response.nonce.is_none() { + if self.protocol_version.supports_aes256_gcm_siv() && remote_response.nonce.is_none() { return Err(HandshakeError::MissingNonceForCurrentKey); } @@ -249,6 +246,7 @@ impl<'a, S, R> State<'a, S, R> { .chain(self.ephemeral_keypair.public_key().to_bytes()) .collect(); + #[allow(clippy::unwrap_used)] self.remote_pubkey .as_ref() .unwrap() @@ -261,7 +259,10 @@ impl<'a, S, R> State<'a, S, R> { self.remote_pubkey = Some(remote_pubkey) } - fn on_wg_msg(msg: Option) -> Result>, HandshakeError> { + #[allow(clippy::complexity)] + fn on_wg_msg( + msg: Option, + ) -> Result, Option)>, HandshakeError> { let Some(msg) = msg else { return Err(HandshakeError::ClosedStream); }; @@ -277,9 +278,10 @@ impl<'a, S, R> State<'a, S, R> { // hehe, that's a bit disgusting that the type system requires we explicitly ignore the // protocol_version field that we actually never attach at this point // yet another reason for the overdue refactor - types::RegistrationHandshake::HandshakePayload { data, .. } => { - Ok(Some(data)) - } + types::RegistrationHandshake::HandshakePayload { + protocol_version, + data, + } => Ok(Some((data, protocol_version))), types::RegistrationHandshake::HandshakeError { message } => { Err(HandshakeError::RemoteError(message)) } @@ -299,7 +301,9 @@ impl<'a, S, R> State<'a, S, R> { } #[cfg(not(target_arch = "wasm32"))] - async fn _receive_handshake_message_bytes(&mut self) -> Result, HandshakeError> + async fn _receive_handshake_message_bytes( + &mut self, + ) -> Result<(Vec, Option), HandshakeError> where S: Stream + Unpin, { @@ -318,7 +322,9 @@ impl<'a, S, R> State<'a, S, R> { } #[cfg(target_arch = "wasm32")] - async fn _receive_handshake_message_bytes(&mut self) -> Result, HandshakeError> + async fn _receive_handshake_message_bytes( + &mut self, + ) -> Result<(Vec, Option), HandshakeError> where S: Stream + Unpin, { @@ -331,20 +337,22 @@ impl<'a, S, R> State<'a, S, R> { } } - pub(crate) async fn receive_handshake_message(&mut self) -> Result + pub(crate) async fn receive_handshake_message( + &mut self, + ) -> Result<(M, Option), HandshakeError> where S: Stream + Unpin, M: HandshakeMessage, { // TODO: make timeout duration configurable - let bytes = timeout( + let (bytes, protocol) = timeout( Duration::from_secs(5), self._receive_handshake_message_bytes(), ) .await .map_err(|_| HandshakeError::Timeout)??; - M::try_from_bytes(&bytes) + M::try_from_bytes(&bytes).map(|msg| (msg, protocol)) } // upon receiving this, the receiver should terminate the handshake @@ -357,21 +365,11 @@ impl<'a, S, R> State<'a, S, R> { { let handshake_message = types::RegistrationHandshake::new_error(message); self.ws_stream - .send(WsMessage::Text(handshake_message.try_into().unwrap())) + .send(WsMessage::Text(handshake_message.into())) .await .map_err(|_| HandshakeError::ClosedStream) } - fn request_protocol_version(&self) -> u8 { - if self.derive_aes256_gcm_siv_key { - AES_GCM_SIV_PROTOCOL_VERSION - } else if self.expects_credential_usage { - CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION - } else { - INITIAL_PROTOCOL_VERSION - } - } - pub(crate) async fn send_handshake_data( &mut self, inner_message: M, @@ -384,18 +382,25 @@ impl<'a, S, R> State<'a, S, R> { let handshake_message = types::RegistrationHandshake::new_payload( inner_message.into_bytes(), - self.request_protocol_version(), + self.protocol_version, ); self.ws_stream - .send(WsMessage::Text(handshake_message.try_into().unwrap())) + .send(WsMessage::Text(handshake_message.into())) .await .map_err(|_| HandshakeError::ClosedStream) } /// Finish the handshake, yielding the derived shared key and implicitly dropping all borrowed /// values. - pub(crate) fn finalize_handshake(self) -> SharedGatewayKey { - self.derived_shared_keys.unwrap() + pub(crate) fn finalize_handshake(self) -> HandshakeResult { + // SAFETY: handshake can't be finalised without deriving the shared keys + #[allow(clippy::unwrap_used)] + HandshakeResult { + negotiated_protocol: self + .proposed_protocol_version() + .unwrap_or(INITIAL_PROTOCOL_VERSION), + derived_key: self.derived_shared_keys.unwrap(), + } } // If any step along the way failed (that are non-network related), diff --git a/common/gateway-requests/src/shared_key/legacy.rs b/common/gateway-requests/src/shared_key/legacy.rs index 8fcf286697..6f40fcd2ff 100644 --- a/common/gateway-requests/src/shared_key/legacy.rs +++ b/common/gateway-requests/src/shared_key/legacy.rs @@ -43,6 +43,7 @@ impl LegacySharedKeys { rng.fill_bytes(&mut salt); let legacy_bytes = Zeroizing::new(self.to_bytes()); + #[allow(clippy::expect_used)] let okm = hkdf::extract_then_expand::( Some(&salt), &legacy_bytes, @@ -51,6 +52,7 @@ impl LegacySharedKeys { ) .expect("somehow too long okm was provided"); + #[allow(clippy::expect_used)] let key = SharedSymmetricKey::try_from_bytes(&okm) .expect("okm was expanded to incorrect length!"); (key, salt) @@ -62,6 +64,7 @@ impl LegacySharedKeys { expected_digest: &[u8], ) -> Option { let legacy_bytes = Zeroizing::new(self.to_bytes()); + #[allow(clippy::expect_used)] let okm = hkdf::extract_then_expand::( Some(salt), &legacy_bytes, @@ -69,6 +72,8 @@ impl LegacySharedKeys { SharedKeySize::to_usize(), ) .expect("somehow too long okm was provided"); + + #[allow(clippy::expect_used)] let key = SharedSymmetricKey::try_from_bytes(&okm) .expect("okm was expanded to incorrect length!"); if key.digest() != expected_digest { diff --git a/common/gateway-requests/src/shared_key/mod.rs b/common/gateway-requests/src/shared_key/mod.rs index b424fd49bf..c0a72135a3 100644 --- a/common/gateway-requests/src/shared_key/mod.rs +++ b/common/gateway-requests/src/shared_key/mod.rs @@ -47,6 +47,8 @@ impl SharedGatewayKey { } } + // it is responsibility of the caller to ensure the correct variant is present + #[allow(clippy::panic)] pub fn unwrap_legacy(&self) -> &LegacySharedKeys { match self { SharedGatewayKey::Current(_) => panic!("expected legacy key"), diff --git a/common/gateway-requests/src/types/error.rs b/common/gateway-requests/src/types/error.rs index edd8e41b22..bdbfe6fbac 100644 --- a/common/gateway-requests/src/types/error.rs +++ b/common/gateway-requests/src/types/error.rs @@ -13,7 +13,7 @@ use thiserror::Error; use time::OffsetDateTime; // specific errors (that should not be nested!!) for clients to match on -#[derive(Debug, Copy, Clone, Error, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Error, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum SimpleGatewayRequestsError { #[error("insufficient bandwidth available to process the request. required: {required}B, available: {available}B")] diff --git a/common/gateway-requests/src/types/registration_handshake_wrapper.rs b/common/gateway-requests/src/types/registration_handshake_wrapper.rs index 0dc6daa567..be75af675b 100644 --- a/common/gateway-requests/src/types/registration_handshake_wrapper.rs +++ b/common/gateway-requests/src/types/registration_handshake_wrapper.rs @@ -1,6 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::GatewayProtocolVersion; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -9,7 +10,7 @@ use std::str::FromStr; pub enum RegistrationHandshake { HandshakePayload { #[serde(default)] - protocol_version: Option, + protocol_version: Option, data: Vec, }, HandshakeError { @@ -18,9 +19,9 @@ pub enum RegistrationHandshake { } impl RegistrationHandshake { - pub fn new_payload(data: Vec, protocol_version: u8) -> Self { + pub fn new_payload(data: Vec, protocol_version: Option) -> Self { RegistrationHandshake::HandshakePayload { - protocol_version: Some(protocol_version), + protocol_version, data, } } @@ -48,11 +49,11 @@ impl TryFrom for RegistrationHandshake { } } -impl TryInto for RegistrationHandshake { - type Error = serde_json::Error; - - fn try_into(self) -> Result { - serde_json::to_string(&self) +impl From for String { + fn from(value: RegistrationHandshake) -> Self { + // SAFETY: we have infallible serde implementation + #[allow(clippy::unwrap_used)] + serde_json::to_string(&value).unwrap() } } @@ -79,7 +80,7 @@ mod tests { assert_eq!(protocol_version, Some(42)); assert_eq!(data, handshake_data) } - _ => unreachable!("this branch shouldn't have been reached!"), + _ => panic!("this branch shouldn't have been reached!"), } let handshake_payload_without_protocol = RegistrationHandshake::HandshakePayload { @@ -97,7 +98,7 @@ mod tests { assert!(protocol_version.is_none()); assert_eq!(data, handshake_data) } - _ => unreachable!("this branch shouldn't have been reached!"), + _ => panic!("this branch shouldn't have been reached!"), } } } diff --git a/common/gateway-requests/src/types/text_request/authenticate.rs b/common/gateway-requests/src/types/text_request/authenticate.rs index 6c4c884957..65dacb3f00 100644 --- a/common/gateway-requests/src/types/text_request/authenticate.rs +++ b/common/gateway-requests/src/types/text_request/authenticate.rs @@ -1,7 +1,9 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::{AuthenticationFailure, GatewayRequestsError, SharedGatewayKey}; +use crate::{ + AuthenticationFailure, GatewayProtocolVersion, GatewayRequestsError, SharedGatewayKey, +}; use nym_crypto::asymmetric::ed25519; use serde::{Deserialize, Serialize}; use std::iter; @@ -20,7 +22,7 @@ pub struct AuthenticateRequest { impl AuthenticateRequest { pub fn new( - protocol_version: u8, + protocol_version: GatewayProtocolVersion, shared_key: &SharedGatewayKey, identity_keys: &ed25519::KeyPair, ) -> Result { @@ -98,7 +100,7 @@ impl AuthenticateRequest { #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct AuthenticateRequestContent { - pub protocol_version: u8, + pub protocol_version: GatewayProtocolVersion, // this is identical to the client's address pub client_identity: ed25519::PublicKey, diff --git a/common/gateway-requests/src/types/text_request/mod.rs b/common/gateway-requests/src/types/text_request/mod.rs index 342bc9adfd..b15a27bf93 100644 --- a/common/gateway-requests/src/types/text_request/mod.rs +++ b/common/gateway-requests/src/types/text_request/mod.rs @@ -4,9 +4,8 @@ use crate::models::CredentialSpendingRequest; use crate::text_request::authenticate::AuthenticateRequest; use crate::{ - GatewayRequestsError, SharedGatewayKey, SymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION, - AUTHENTICATE_V2_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, - INITIAL_PROTOCOL_VERSION, + GatewayProtocolVersion, GatewayRequestsError, SharedGatewayKey, SymmetricKey, + AES_GCM_SIV_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, }; use nym_credentials_interface::CredentialSpendingData; use nym_crypto::asymmetric::ed25519; @@ -46,6 +45,7 @@ impl ClientRequest { // - the schema is self-describing which simplifies deserialisation // SAFETY: the trait has been derived correctly with no weird variants + #[allow(clippy::unwrap_used)] let plaintext = serde_json::to_vec(self).unwrap(); let nonce = key.random_nonce_or_iv(); let ciphertext = key.encrypt(&plaintext, Some(&nonce))?; @@ -72,7 +72,7 @@ pub enum ClientControlRequest { // have the shared key derived? Authenticate { #[serde(default)] - protocol_version: Option, + protocol_version: Option, address: String, enc_address: String, iv: String, @@ -83,7 +83,7 @@ pub enum ClientControlRequest { #[serde(alias = "handshakePayload")] RegisterHandshakeInitRequest { #[serde(default)] - protocol_version: Option, + protocol_version: Option, data: Vec, }, BandwidthCredential { @@ -98,6 +98,10 @@ pub enum ClientControlRequest { enc_credential: Vec, iv: Vec, }, + UpgradeModeJWT { + // no need to encrypt it as it's public anyway + token: String, + }, ClaimFreeTestnetBandwidth, EncryptedRequest { ciphertext: Vec, @@ -108,12 +112,14 @@ pub enum ClientControlRequest { } impl ClientControlRequest { - pub fn new_authenticate( + pub fn new_legacy_authenticate( address: DestinationAddressBytes, shared_key: &SharedGatewayKey, uses_credentials: bool, ) -> Result { // if we're encrypting with non-legacy key, the remote must support AES256-GCM-SIV + // since we are using legacy authentication, the gateway definitely doesn't understand the protocol downgrade, + // so use the lowest possible version we can let protocol_version = if !shared_key.is_legacy() { Some(AES_GCM_SIV_PROTOCOL_VERSION) } else if uses_credentials { @@ -138,10 +144,8 @@ impl ClientControlRequest { pub fn new_authenticate_v2( shared_key: &SharedGatewayKey, identity_keys: &ed25519::KeyPair, + protocol_version: GatewayProtocolVersion, ) -> Result { - // if we're using v2 authentication, we must announce at least that protocol version - let protocol_version = AUTHENTICATE_V2_PROTOCOL_VERSION; - Ok(ClientControlRequest::AuthenticateV2(Box::new( AuthenticateRequest::new(protocol_version, shared_key, identity_keys)?, ))) @@ -159,6 +163,7 @@ impl ClientControlRequest { "BandwidthCredentialV2".to_string() } ClientControlRequest::EcashCredential { .. } => "EcashCredential".to_string(), + ClientControlRequest::UpgradeModeJWT { .. } => "UpgradeModeJWT".to_string(), ClientControlRequest::ClaimFreeTestnetBandwidth => { "ClaimFreeTestnetBandwidth".to_string() } @@ -192,12 +197,16 @@ impl ClientControlRequest { CredentialSpendingRequest::try_from_bytes(credential_bytes.as_slice()) .map_err(|_| GatewayRequestsError::MalformedEncryption) } + + pub fn new_upgrade_mode_jwt(token: String) -> Self { + ClientControlRequest::UpgradeModeJWT { token } + } } impl From for Message { fn from(req: ClientControlRequest) -> Self { - // it should be safe to call `unwrap` here as the message is generated by the server - // so if it fails (and consequently panics) it's a bug that should be resolved + // SAFETY: all of the enum variants have valid (for json) serde impl + #[allow(clippy::unwrap_used)] let str_req = serde_json::to_string(&req).unwrap(); Message::Text(str_req) } diff --git a/common/gateway-requests/src/types/text_response.rs b/common/gateway-requests/src/types/text_response.rs index c3418649cf..140aa26bbd 100644 --- a/common/gateway-requests/src/types/text_response.rs +++ b/common/gateway-requests/src/types/text_response.rs @@ -1,7 +1,9 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{GatewayRequestsError, SimpleGatewayRequestsError, SymmetricKey}; +use crate::{ + GatewayProtocolVersion, GatewayRequestsError, SimpleGatewayRequestsError, SymmetricKey, +}; use serde::{Deserialize, Serialize}; use tungstenite::Message; @@ -26,6 +28,7 @@ impl SensitiveServerResponse { // - the schema is self-describing which simplifies deserialisation // SAFETY: the trait has been derived correctly with no weird variants + #[allow(clippy::unwrap_used)] let plaintext = serde_json::to_vec(self).unwrap(); let nonce = key.random_nonce_or_iv(); let ciphertext = key.encrypt(&plaintext, Some(&nonce))?; @@ -43,31 +46,57 @@ impl SensitiveServerResponse { } } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct BandwidthResponse { + pub available_total: i64, + + /// Flag indicating whether the gateway has detected the system is undergoing the upgrade + /// (thus it will not meter bandwidth) + #[serde(default)] + pub upgrade_mode: bool, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct SendResponse { + pub remaining_bandwidth: i64, + + /// Flag indicating whether the gateway has detected the system is undergoing the upgrade + /// (thus it will not meter bandwidth) + #[serde(default)] + pub upgrade_mode: bool, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] #[serde(tag = "type", rename_all = "camelCase")] #[non_exhaustive] pub enum ServerResponse { Authenticate { #[serde(default)] - protocol_version: Option, + protocol_version: Option, status: bool, bandwidth_remaining: i64, + + /// Flag indicating whether the gateway has detected the system is undergoing the upgrade + /// (thus it will not meter bandwidth) + #[serde(default)] + upgrade_mode: bool, }, Register { #[serde(default)] - protocol_version: Option, + protocol_version: Option, status: bool, + + /// Flag indicating whether the gateway has detected the system is undergoing the upgrade + /// (thus it will not meter bandwidth) + #[serde(default)] + upgrade_mode: bool, }, EncryptedResponse { ciphertext: Vec, nonce: Vec, }, - Bandwidth { - available_total: i64, - }, - Send { - remaining_bandwidth: i64, - }, + Bandwidth(BandwidthResponse), + Send(SendResponse), SupportedProtocol { version: u8, }, @@ -122,6 +151,7 @@ impl From for Message { fn from(res: ServerResponse) -> Self { // it should be safe to call `unwrap` here as the message is generated by the server // so if it fails (and consequently panics) it's a bug that should be resolved + #[allow(clippy::unwrap_used)] let str_res = serde_json::to_string(&res).unwrap(); Message::Text(str_res) } @@ -134,3 +164,79 @@ impl TryFrom for ServerResponse { serde_json::from_str(&msg) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn server_response_serde_compat() { + // make sure new serialisation is identical and compatible + #[derive(Serialize, Deserialize, Debug, PartialEq)] + #[serde(tag = "type", rename_all = "camelCase")] + #[non_exhaustive] + pub enum OldServerResponse { + Bandwidth { available_total: i64 }, + Send { remaining_bandwidth: i64 }, + } + + // OLD => NEW + let old_bandwidth = OldServerResponse::Bandwidth { + available_total: 100, + }; + let old_send = OldServerResponse::Send { + remaining_bandwidth: 100, + }; + + let old_bandwidth_str = serde_json::to_string(&old_bandwidth).unwrap(); + let old_send_str = serde_json::to_string(&old_send).unwrap(); + + let recovered_bandwidth = ServerResponse::try_from(old_bandwidth_str).unwrap(); + assert_eq!( + recovered_bandwidth, + ServerResponse::Bandwidth(BandwidthResponse { + available_total: 100, + upgrade_mode: false + }) + ); + + let recovered_send = ServerResponse::try_from(old_send_str).unwrap(); + assert_eq!( + recovered_send, + ServerResponse::Send(SendResponse { + remaining_bandwidth: 100, + upgrade_mode: false + }) + ); + + // NEW => OLD + let new_bandwidth = ServerResponse::Bandwidth(BandwidthResponse { + available_total: 100, + upgrade_mode: false, + }); + let new_send = ServerResponse::Send(SendResponse { + remaining_bandwidth: 100, + upgrade_mode: false, + }); + + let new_bandwidth_str = serde_json::to_string(&new_bandwidth).unwrap(); + let new_send_str = serde_json::to_string(&new_send).unwrap(); + + let recovered_bandwidth: OldServerResponse = + serde_json::from_str(&new_bandwidth_str).unwrap(); + assert_eq!( + recovered_bandwidth, + OldServerResponse::Bandwidth { + available_total: 100 + } + ); + + let recovered_send: OldServerResponse = serde_json::from_str(&new_send_str).unwrap(); + assert_eq!( + recovered_send, + OldServerResponse::Send { + remaining_bandwidth: 100 + } + ); + } +} diff --git a/common/service-provider-requests-common/src/lib.rs b/common/service-provider-requests-common/src/lib.rs index 9e392dff19..44e54dc2af 100644 --- a/common/service-provider-requests-common/src/lib.rs +++ b/common/service-provider-requests-common/src/lib.rs @@ -82,6 +82,17 @@ pub struct Protocol { pub service_provider_type: ServiceProviderType, } +impl Protocol { + pub const fn new(version: u8, service_provider_type: ServiceProviderType) -> Self { + Self { + version, + service_provider_type, + } + } +} + +// NOTE: this only works under the assumption of using bincode for serialisation +// with the current field layout impl TryFrom<&[u8; 2]> for Protocol { type Error = ProtocolError; diff --git a/common/upgrade-mode-check/src/error.rs b/common/upgrade-mode-check/src/error.rs index 0edf4201e8..3b2f2b5b12 100644 --- a/common/upgrade-mode-check/src/error.rs +++ b/common/upgrade-mode-check/src/error.rs @@ -6,7 +6,7 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum UpgradeModeCheckError { - #[error("failed to decode jwt metadata")] + #[error("failed to decode jwt metadata: {source}")] TokenMetadataDecodeFailure { source: jwt_simple::Error }, #[error("the jwt metadata didn't contain explicit public key")] @@ -21,6 +21,6 @@ pub enum UpgradeModeCheckError { #[error("failed to verify the jwt: {source}")] JwtVerificationFailure { source: jwt_simple::Error }, - #[error("failed to retrieve attestation from {url}:{source}")] + #[error("failed to retrieve attestation from {url}: {source}")] AttestationRetrievalFailure { url: String, source: reqwest::Error }, } diff --git a/common/upgrade-mode-check/src/jwt.rs b/common/upgrade-mode-check/src/jwt.rs index 060546f0bb..64c436b23e 100644 --- a/common/upgrade-mode-check/src/jwt.rs +++ b/common/upgrade-mode-check/src/jwt.rs @@ -10,6 +10,8 @@ use nym_crypto::asymmetric::ed25519; use std::collections::HashSet; use std::time::Duration; +pub const CREDENTIAL_PROXY_JWT_ISSUER: &str = "nym-credential-proxy"; + // for now use static issuer such as "nym-credential-proxy" pub fn generate_jwt_for_upgrade_mode_attestation( attestation: UpgradeModeAttestation, @@ -109,11 +111,11 @@ mod tests { attestation.clone(), Duration::from_secs(60 * 60), &unauthorised_jwt_keys, - Some("nym-credential-proxy"), + Some(CREDENTIAL_PROXY_JWT_ISSUER), ); // we expect 'nym-credential-proxy' issuer - assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("nym-credential-proxy")).is_ok()); + assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some(CREDENTIAL_PROXY_JWT_ISSUER)).is_ok()); // we don't care about issuer assert!(validate_upgrade_mode_jwt(&jwt_issuer, None).is_ok()); @@ -133,7 +135,9 @@ mod tests { None, ); // we expect 'nym-credential-proxy' issuer - assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, Some("nym-credential-proxy")).is_err()); + assert!( + validate_upgrade_mode_jwt(&jwt_no_issuer, Some(CREDENTIAL_PROXY_JWT_ISSUER)).is_err() + ); // we don't care about issuer assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, None).is_ok()); diff --git a/common/upgrade-mode-check/src/lib.rs b/common/upgrade-mode-check/src/lib.rs index 36cbf43c91..c2ab50284b 100644 --- a/common/upgrade-mode-check/src/lib.rs +++ b/common/upgrade-mode-check/src/lib.rs @@ -9,7 +9,10 @@ pub use attestation::{ UpgradeModeAttestation, generate_new_attestation, generate_new_attestation_with_starting_time, }; pub use error::UpgradeModeCheckError; -pub use jwt::{generate_jwt_for_upgrade_mode_attestation, validate_upgrade_mode_jwt}; +pub use jwt::{ + CREDENTIAL_PROXY_JWT_ISSUER, generate_jwt_for_upgrade_mode_attestation, + validate_upgrade_mode_jwt, +}; #[cfg(not(target_arch = "wasm32"))] pub use attestation::attempt_retrieve_attestation; diff --git a/common/wireguard-private-metadata/client/src/lib.rs b/common/wireguard-private-metadata/client/src/lib.rs index 58d78fb6c6..3dd884518c 100644 --- a/common/wireguard-private-metadata/client/src/lib.rs +++ b/common/wireguard-private-metadata/client/src/lib.rs @@ -46,6 +46,23 @@ pub trait WireguardMetadataApiClient: ApiClient { ) .await } + + #[instrument(level = "debug", skip(self, request_body))] + async fn request_upgrade_mode_check( + &self, + request_body: &Request, + ) -> Result { + self.post_json( + &[ + routes::V1_API_VERSION, + routes::NETWORK, + routes::UPGRADE_MODE_CHECK, + ], + NO_PARAMS, + request_body, + ) + .await + } } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] diff --git a/common/wireguard-private-metadata/server/src/http/router.rs b/common/wireguard-private-metadata/server/src/http/router.rs index 099e1d5724..e76df7d328 100644 --- a/common/wireguard-private-metadata/server/src/http/router.rs +++ b/common/wireguard-private-metadata/server/src/http/router.rs @@ -15,7 +15,7 @@ use utoipa_swagger_ui::SwaggerUi; use crate::http::openapi::ApiDoc; use crate::http::state::AppState; -use crate::network::bandwidth_routes; +use crate::network::{bandwidth_routes, network_routes}; /// Wrapper around `axum::Router` which ensures correct [order of layers][order]. /// Add new routes as if you were working directly with `axum`. @@ -35,7 +35,12 @@ impl RouterBuilder { let default_routes = Router::new() .merge(SwaggerUi::new("/swagger").url("/api-docs/openapi.json", ApiDoc::openapi())) .route("/", get(|| async { Redirect::to("/swagger") })) - .nest("/v1", Router::new().nest("/bandwidth", bandwidth_routes())); + .nest( + "/v1", + Router::new() + .nest("/bandwidth", bandwidth_routes()) + .nest("/network", network_routes()), + ); Self { unfinished_router: default_routes, } diff --git a/common/wireguard-private-metadata/server/src/http/state.rs b/common/wireguard-private-metadata/server/src/http/state.rs index 06916bb35f..3e913d7073 100644 --- a/common/wireguard-private-metadata/server/src/http/state.rs +++ b/common/wireguard-private-metadata/server/src/http/state.rs @@ -1,35 +1,137 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use nym_credential_verification::upgrade_mode::UpgradeModeDetails; +use nym_credentials_interface::BandwidthCredential; +use std::cmp::max; use std::net::IpAddr; -use nym_credentials_interface::CredentialSpendingData; - use crate::transceiver::PeerControllerTransceiver; use nym_wireguard_private_metadata_shared::error::MetadataError; +use nym_wireguard_private_metadata_shared::interface::{ResponseData, UpgradeModeCheckRequestType}; + +// we need to be above MINIMUM_REMAINING_BANDWIDTH (500MB) plus we also have to trick the client +// its depletion is low enough to not require sending new tickets +const DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD: i64 = 1024 * 1024 * 1024; #[derive(Clone, axum::extract::FromRef)] pub struct AppState { transceiver: PeerControllerTransceiver, + #[from_ref(skip)] + upgrade_mode: UpgradeModeDetails, } impl AppState { - pub fn new(transceiver: PeerControllerTransceiver) -> Self { - Self { transceiver } + pub fn new(transceiver: PeerControllerTransceiver, upgrade_mode: UpgradeModeDetails) -> Self { + Self { + transceiver, + upgrade_mode, + } } - pub async fn available_bandwidth(&self, ip: IpAddr) -> Result { - self.transceiver.query_bandwidth(ip).await + fn upgrade_mode_bandwidth(&self, true_bandwidth: i64) -> i64 { + // if we're undergoing upgrade mode, we don't meter bandwidth, + // we simply return MAX of clients current bandwidth and minimum bandwidth before default + // client would have attempted to send new ticket (hopefully) + // the latter is to support older clients that will ignore `upgrade_mode` field in the response + // as they're not aware of its existence + max(DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD, true_bandwidth) + } + + pub async fn available_bandwidth(&self, ip: IpAddr) -> Result { + let upgrade_mode = self.upgrade_mode.enabled(); + + let true_bandwidth = self.transceiver.query_bandwidth(ip).await?; + let available_bandwidth = if upgrade_mode { + self.upgrade_mode_bandwidth(true_bandwidth) + } else { + true_bandwidth + }; + + Ok(ResponseData::AvailableBandwidth { + amount: available_bandwidth, + upgrade_mode, + }) } // Top up with a credential and return the afterwards available bandwidth pub async fn topup_bandwidth( &self, ip: IpAddr, - credential: CredentialSpendingData, - ) -> Result { - self.transceiver - .topup_bandwidth(ip, Box::new(credential)) - .await + claim: Box, + ) -> Result { + match *claim { + BandwidthCredential::ZkNym(zk_nym) => { + // if we got zk-nym, we just try to verify it + let available_bandwidth = self.transceiver.topup_bandwidth(ip, zk_nym).await?; + + // however, we still follow the same upgrade-mode logic, + // so that the client would not attempt to needlessly send more credentials + let upgrade_mode = self.upgrade_mode.enabled(); + let available_bandwidth = if upgrade_mode { + self.upgrade_mode_bandwidth(available_bandwidth) + } else { + available_bandwidth + }; + + Ok(ResponseData::TopUpBandwidth { + available_bandwidth, + upgrade_mode, + }) + } + BandwidthCredential::UpgradeModeJWT { token } => { + // if we're already in the upgrade mode, don't bother validating the token + if self.upgrade_mode.enabled() { + let true_bandwidth = self.transceiver.query_bandwidth(ip).await?; + return Ok(ResponseData::TopUpBandwidth { + available_bandwidth: self.upgrade_mode_bandwidth(true_bandwidth), + upgrade_mode: true, + }); + } + + // if the token is valid, try to check if we're behind + // and have to update our internal state + self.upgrade_mode + .try_enable_via_received_jwt(token) + .await + .map_err(|err| MetadataError::JWTVerification { + message: err.to_string(), + })?; + + // if we didn't return an error, it means token got accepted + // and we have transitioned into the upgrade mode + let true_bandwidth = self.transceiver.query_bandwidth(ip).await?; + + Ok(ResponseData::TopUpBandwidth { + available_bandwidth: self.upgrade_mode_bandwidth(true_bandwidth), + upgrade_mode: true, + }) + } + } + } + + pub async fn upgrade_mode_check( + &self, + request: UpgradeModeCheckRequestType, + ) -> Result { + // if we're already in the upgrade mode - no need to do anything + if self.upgrade_mode.enabled() { + return Ok(ResponseData::UpgradeMode { upgrade_mode: true }); + } + + match request { + UpgradeModeCheckRequestType::UpgradeModeJwt { token } => { + self.upgrade_mode + .try_enable_via_received_jwt(token) + .await + .map_err(|err| MetadataError::JWTVerification { + message: err.to_string(), + })?; + } + } + + // if we didn't return an error, it means token got accepted + // and we have transitioned into the upgrade mode + Ok(ResponseData::UpgradeMode { upgrade_mode: true }) } } diff --git a/common/wireguard-private-metadata/server/src/network.rs b/common/wireguard-private-metadata/server/src/network.rs index 8e27eca5cd..f1ad08a108 100644 --- a/common/wireguard-private-metadata/server/src/network.rs +++ b/common/wireguard-private-metadata/server/src/network.rs @@ -9,8 +9,7 @@ use axum::{ }; use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_wireguard_private_metadata_shared::{ - AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, - interface::{RequestData, ResponseData}, + AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, interface::RequestData, latest, }; use tower_http::compression::CompressionLayer; @@ -25,6 +24,15 @@ pub(crate) fn bandwidth_routes() -> Router { .layer(CompressionLayer::new()) } +pub(crate) fn network_routes() -> Router { + Router::new() + .route( + "/upgrade-mode-check", + axum::routing::post(upgrade_mode_check), + ) + .layer(CompressionLayer::new()) +} + #[utoipa::path( tag = "bandwidth", get, @@ -59,20 +67,17 @@ async fn available_bandwidth( ) -> AxumResult> { let output = output.output.unwrap_or_default(); - let (RequestData::AvailableBandwidth(_), version) = + let (RequestData::AvailableBandwidth, version) = request.extract().map_err(AxumErrorResponse::bad_request)? else { return Err(AxumErrorResponse::bad_request("incorrect request type")); }; - let available_bandwidth = state + let available_bandwidth_response = state .available_bandwidth(addr.ip()) .await .map_err(AxumErrorResponse::bad_request)?; - let response = Response::construct( - ResponseData::AvailableBandwidth(available_bandwidth), - version, - ) - .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct(available_bandwidth_response, version) + .map_err(AxumErrorResponse::bad_request)?; Ok(output.to_response(response)) } @@ -96,16 +101,49 @@ async fn topup_bandwidth( ) -> AxumResult> { let output = output.output.unwrap_or_default(); - let (RequestData::TopUpBandwidth(credential), version) = + let (RequestData::TopUpBandwidth { credential }, version) = request.extract().map_err(AxumErrorResponse::bad_request)? else { return Err(AxumErrorResponse::bad_request("incorrect request type")); }; - let available_bandwidth = state - .topup_bandwidth(addr.ip(), *credential) + let top_up_bandwidth_response = state + .topup_bandwidth(addr.ip(), credential) .await .map_err(AxumErrorResponse::bad_request)?; - let response = Response::construct(ResponseData::TopUpBandwidth(available_bandwidth), version) + let response = Response::construct(top_up_bandwidth_response, version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) +} + +#[utoipa::path( + tag = "network", + post, + request_body = Request, + path = "/v1/network/upgrade-mode-check", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] +async fn upgrade_mode_check( + Query(output): Query, + State(state): State, + Json(request): Json, +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::UpgradeModeCheck { typ }, version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let upgrade_mode_check_response = state + .upgrade_mode_check(typ) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct(upgrade_mode_check_response, version) .map_err(AxumErrorResponse::bad_request)?; Ok(output.to_response(response)) diff --git a/common/wireguard-private-metadata/server/src/transceiver.rs b/common/wireguard-private-metadata/server/src/transceiver.rs index cbe77126cf..5614e7f8f7 100644 --- a/common/wireguard-private-metadata/server/src/transceiver.rs +++ b/common/wireguard-private-metadata/server/src/transceiver.rs @@ -37,12 +37,12 @@ impl PeerControllerTransceiver { }) } - pub(crate) async fn query_bandwidth(&self, ip: IpAddr) -> Result { + pub async fn query_bandwidth(&self, ip: IpAddr) -> Result { Ok(self.get_client_bandwidth(ip).await?.available().await) } // Top up with a credential and return the afterwards available bandwidth - pub(crate) async fn topup_bandwidth( + pub async fn topup_bandwidth( &self, ip: IpAddr, credential: Box, diff --git a/common/wireguard-private-metadata/shared/src/conversion_helpers.rs b/common/wireguard-private-metadata/shared/src/conversion_helpers.rs new file mode 100644 index 0000000000..117f9afb02 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/conversion_helpers.rs @@ -0,0 +1,218 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +/// A simple macro that given `TryFrom<&A> for B`, implements `TryFrom for B` +/// using the former implementation +#[macro_export] +macro_rules! impl_tryfrom_ref { + ($src:ty, $dst:ty, $err:ty) => { + impl TryFrom<$src> for $dst { + // can't use type Error = >::Error; + // due to lifetime interference within macros + type Error = $err; + + fn try_from(value: $src) -> Result { + >::try_from(&value) + } + } + }; +} + +/// A simple macro that implements all required variants of `TryFrom` +/// between particular versioned `VersionedRequest` and given request variant +/// using default bincode serializer +#[macro_export] +macro_rules! impl_default_bincode_request_query_conversions { + // limitation of macros - need to pass the same underlying type twice, + // once as pattern and once as expression + ($top_req_type:ty, $inner_req_type:ty, $query_type_pat:pat, $query_type_expr:expr) => { + $crate::impl_query_conversions!( + $crate::Request, + $top_req_type, + $inner_req_type, + $query_type_pat, + $query_type_expr + ); + }; +} + +/// A simple macro that implements all required variants of `TryFrom` +/// between particular versioned `VersionedResponse` and given response variant +/// using default bincode serializer +#[macro_export] +macro_rules! impl_default_bincode_response_query_conversions { + // limitation of macros - need to pass the same underlying type twice, + // once as pattern and once as expression + ($top_resp_type:ty, $inner_resp_type:ty, $query_type_pat:pat, $query_type_expr:expr) => { + $crate::impl_query_conversions!( + $crate::Response, + $top_resp_type, + $inner_resp_type, + $query_type_pat, + $query_type_expr + ); + }; +} + +/// A simple macro that implements all required variants of `TryFrom` +/// between [crate::models::Request] and corresponding versioned `VersionedRequest` +/// using default bincode serializer +#[macro_export] +macro_rules! impl_default_bincode_request_conversions { + ($req_type:ty, $version:expr) => { + $crate::impl_versioned_conversions!($crate::Request, $req_type, $version); + }; +} + +/// A simple macro that implements all required variants of `TryFrom` +/// between [crate::models::Response] and corresponding versioned `VersionedResponse` +/// using default bincode serializer +#[macro_export] +macro_rules! impl_default_bincode_response_conversions { + ($req_type:ty, $version:expr) => { + $crate::impl_versioned_conversions!($crate::Response, $req_type, $version); + }; +} + +#[macro_export] +macro_rules! impl_versioned_conversions { + ( + // is it Request or Response + $main_type_ty:ty, + + // e.g. VersionedResponse + $top_type:ty, + + // request/response version type + $version:expr + ) => { + impl TryFrom<&$top_type> for $main_type_ty { + type Error = $crate::models::error::Error; + + fn try_from(value: &$top_type) -> Result { + use ::bincode::Options; + let data = $crate::make_bincode_serializer().serialize(value)?; + Ok(<$main_type_ty>::new($version, data)) + } + } + + // automatically generate `impl TryFrom<$top_type> for $main_type` + $crate::impl_tryfrom_ref!($top_type, $main_type_ty, $crate::models::error::Error); + + impl TryFrom<&$main_type_ty> for $top_type { + type Error = $crate::models::error::Error; + + fn try_from(value: &$main_type_ty) -> Result { + use ::bincode::Options; + if value.version != $version { + return Err($crate::models::error::Error::InvalidVersion { + source_version: value.version, + target_version: $version, + }); + } + Ok($crate::make_bincode_serializer().deserialize(&value.inner)?) + } + } + + // automatically generate `impl TryFrom<$main_type> for $top_type` + $crate::impl_tryfrom_ref!($main_type_ty, $top_type, $crate::models::error::Error); + }; +} + +#[macro_export] +macro_rules! impl_query_conversions { + // limitation of macros - need to pass the same underlying type twice, + // once as pattern and once as expression + ( + // is it Request or Response + $main_type:ty, + + // e.g. VersionedResponse + $top_type:ty, + + // e.g. InnerTopUpResponse + $inner_type:ty, + + // e.g. QueryType::TopUpBandwidth, + $query_type_pat:pat, + + // e.g. QueryType::TopUpBandwidth, + $query_type_expr:expr + ) => { + // conversion from the versioned type into the particular typ, + // e.g. TryFrom<&VersionedResponse> for InnerTopUpResponse + impl TryFrom<&$top_type> for $inner_type { + type Error = $crate::models::error::Error; + + fn try_from(value: &$top_type) -> Result { + use ::bincode::Options; + match value.query_type { + $query_type_pat => { + Ok($crate::make_bincode_serializer().deserialize(&value.inner)?) + } + other => Err($crate::models::error::Error::InvalidQueryType { + source_query_type: other.to_string(), + target_query_type: stringify!($query_type_pat).to_string(), + }), + } + } + } + // implementation of conversion without the referenced type, i.e. + // e.g. TryFrom for InnerTopUpResponse + $crate::impl_tryfrom_ref!($top_type, $inner_type, $crate::models::error::Error); + + // conversion back from the particular type into the versioned type, i.e. + // e.g. TryFrom<&InnerTopUpResponse> for VersionedResponse + impl TryFrom<&$inner_type> for $top_type { + type Error = $crate::models::error::Error; + + fn try_from(value: &$inner_type) -> Result { + use ::bincode::Options; + Ok(Self { + query_type: $query_type_expr, + inner: $crate::make_bincode_serializer().serialize(value)?, + }) + } + } + + // implementation of conversion without the referenced type, i.e. + // e.g. TryFrom for VersionedResponse + $crate::impl_tryfrom_ref!($inner_type, $top_type, $crate::models::error::Error); + + // conversion from the'main' type (Request/Response) into the particular type + // e.g. TryFrom<&Response> for InnerTopUpResponse + impl TryFrom<&$main_type> for $inner_type { + type Error = $crate::error::MetadataError; + + fn try_from(value: &$main_type) -> Result { + <$top_type>::try_from(value)?.try_into().map_err( + |err: $crate::models::error::Error| $crate::error::MetadataError::Models { + message: err.to_string(), + }, + ) + } + } + + // implementation of conversion without the referenced type, i.e. + // e.g. TryFrom for InnerTopUpResponse + $crate::impl_tryfrom_ref!($main_type, $inner_type, $crate::error::MetadataError); + + // conversion from the particular type into the 'main' type (Request/Response) + // e.g. TryFrom<&InnerTopUpResponse> for Response + impl TryFrom<&$inner_type> for $main_type { + type Error = $crate::error::MetadataError; + + fn try_from(value: &$inner_type) -> Result { + <$top_type>::try_from(value)?.try_into().map_err( + |err: $crate::models::error::Error| $crate::error::MetadataError::Models { + message: err.to_string(), + }, + ) + } + } + + // implementation of conversion without the referenced type, i.e. + // e.g. TryFrom for Response + $crate::impl_tryfrom_ref!($inner_type, $main_type, $crate::error::MetadataError); + }; +} diff --git a/common/wireguard-private-metadata/shared/src/error.rs b/common/wireguard-private-metadata/shared/src/error.rs index 3783462a4d..f8db19c304 100644 --- a/common/wireguard-private-metadata/shared/src/error.rs +++ b/common/wireguard-private-metadata/shared/src/error.rs @@ -17,6 +17,9 @@ pub enum MetadataError { #[error("Credential verification error: {message}")] CredentialVerification { message: String }, + + #[error("Upgrade Mode JWT verification error: {message}")] + JWTVerification { message: String }, } impl From for MetadataError { diff --git a/common/wireguard-private-metadata/shared/src/lib.rs b/common/wireguard-private-metadata/shared/src/lib.rs index 54041fd8c7..c0d711ac42 100644 --- a/common/wireguard-private-metadata/shared/src/lib.rs +++ b/common/wireguard-private-metadata/shared/src/lib.rs @@ -1,6 +1,7 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +pub(crate) mod conversion_helpers; pub mod error; mod models; pub mod routes; @@ -9,7 +10,7 @@ pub mod routes; pub use models::v0; pub use models::{ AxumErrorResponse, AxumResult, Construct, ErrorResponse, Extract, Request, Response, Version, - error::Error as ModelError, interface, latest, v1, + error::Error as ModelError, interface, latest, v1, v2, }; fn make_bincode_serializer() -> impl bincode::Options { diff --git a/common/wireguard-private-metadata/shared/src/models/error.rs b/common/wireguard-private-metadata/shared/src/models/error.rs index 45dc88617d..aef1ac5ae6 100644 --- a/common/wireguard-private-metadata/shared/src/models/error.rs +++ b/common/wireguard-private-metadata/shared/src/models/error.rs @@ -15,7 +15,7 @@ pub enum Error { }, #[error( - "trying to deserialize from query type {source_query_type} query type {target_query_type}" + "trying to deserialize from query type {source_query_type} into query type {target_query_type}" )] InvalidQueryType { source_query_type: String, diff --git a/common/wireguard-private-metadata/shared/src/models/interface.rs b/common/wireguard-private-metadata/shared/src/models/interface.rs index 9d5a786c53..c97db77d2a 100644 --- a/common/wireguard-private-metadata/shared/src/models/interface.rs +++ b/common/wireguard-private-metadata/shared/src/models/interface.rs @@ -1,61 +1,90 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_credentials_interface::CredentialSpendingData; +use nym_credentials_interface::BandwidthCredential; #[cfg(feature = "testing")] use crate::models::v0; -use crate::models::{Construct, Extract, Request, Response, Version, v1}; +use crate::models::{Construct, Extract, Request, Response, Version, latest, v1, v2}; + +pub use latest::check_upgrade_mode::request::UpgradeModeCheckRequestType; pub enum RequestData { - AvailableBandwidth(()), - TopUpBandwidth(Box), + AvailableBandwidth, + TopUpBandwidth { + credential: Box, + }, + UpgradeModeCheck { + typ: UpgradeModeCheckRequestType, + }, } -impl From for RequestData { - fn from(value: super::latest::interface::RequestData) -> Self { +impl From for RequestData { + fn from(value: latest::interface::RequestData) -> Self { match value { - super::latest::interface::RequestData::AvailableBandwidth(inner) => { - Self::AvailableBandwidth(inner) + latest::interface::RequestData::AvailableBandwidth => Self::AvailableBandwidth, + latest::interface::RequestData::TopUpBandwidth { credential } => { + Self::TopUpBandwidth { credential } } - super::latest::interface::RequestData::TopUpBandwidth(credential_spending_data) => { - Self::TopUpBandwidth(credential_spending_data) + latest::interface::RequestData::UpgradeModeCheck { typ } => { + Self::UpgradeModeCheck { typ } } } } } -impl From for super::latest::interface::RequestData { +impl From for latest::interface::RequestData { fn from(value: RequestData) -> Self { match value { - RequestData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), - RequestData::TopUpBandwidth(credential_spending_data) => { - Self::TopUpBandwidth(credential_spending_data) - } + RequestData::AvailableBandwidth => Self::AvailableBandwidth, + RequestData::TopUpBandwidth { credential } => Self::TopUpBandwidth { credential }, + RequestData::UpgradeModeCheck { typ } => Self::UpgradeModeCheck { typ }, } } } -impl From for ResponseData { - fn from(value: super::latest::interface::ResponseData) -> Self { +impl From for ResponseData { + fn from(value: latest::interface::ResponseData) -> Self { match value { - super::latest::interface::ResponseData::AvailableBandwidth(inner) => { - Self::AvailableBandwidth(inner) - } - super::latest::interface::ResponseData::TopUpBandwidth(credential_spending_data) => { - Self::TopUpBandwidth(credential_spending_data) + latest::interface::ResponseData::AvailableBandwidth { + amount, + upgrade_mode, + } => Self::AvailableBandwidth { + amount, + upgrade_mode, + }, + latest::interface::ResponseData::TopUpBandwidth { + available_bandwidth, + upgrade_mode, + } => Self::TopUpBandwidth { + available_bandwidth, + upgrade_mode, + }, + latest::interface::ResponseData::UpgradeMode { upgrade_mode } => { + Self::UpgradeMode { upgrade_mode } } } } } -impl From for super::latest::interface::ResponseData { +impl From for latest::interface::ResponseData { fn from(value: ResponseData) -> Self { match value { - ResponseData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), - ResponseData::TopUpBandwidth(credential_spending_data) => { - Self::TopUpBandwidth(credential_spending_data) - } + ResponseData::AvailableBandwidth { + amount, + upgrade_mode, + } => Self::AvailableBandwidth { + amount, + upgrade_mode, + }, + ResponseData::TopUpBandwidth { + available_bandwidth, + upgrade_mode, + } => Self::TopUpBandwidth { + available_bandwidth, + upgrade_mode, + }, + ResponseData::UpgradeMode { upgrade_mode } => Self::UpgradeMode { upgrade_mode }, } } } @@ -65,13 +94,26 @@ impl Construct for Request { match version { #[cfg(feature = "testing")] Version::V0 => { - let translate_info = super::latest::interface::RequestData::from(info); - let downgrade_info = v0::interface::RequestData::try_from(translate_info)?; - let versioned_request = v0::VersionedRequest::construct(downgrade_info, version)?; + // attempt to go through conversion chain for `info`: v2 => v1 => v0 + let v2_info = v2::interface::RequestData::from(info); + let v1_info = v1::interface::RequestData::try_from(v2_info)?; + let v0_info = v0::interface::RequestData::try_from(v1_info)?; + + let versioned_request = v0::VersionedRequest::construct(v0_info, version)?; Ok(versioned_request.try_into()?) } Version::V1 => { - let versioned_request = v1::VersionedRequest::construct(info.into(), version)?; + // attempt to go through conversion chain for `info`: v2 => v1 + let v2_info = v2::interface::RequestData::from(info); + let v1_info = v1::interface::RequestData::try_from(v2_info)?; + + let versioned_request = v1::VersionedRequest::construct(v1_info, version)?; + Ok(versioned_request.try_into()?) + } + Version::V2 => { + let v2_info = v2::interface::RequestData::from(info); + + let versioned_request = v2::VersionedRequest::construct(v2_info, version)?; Ok(versioned_request.try_into()?) } } @@ -84,24 +126,45 @@ impl Extract for Request { #[cfg(feature = "testing")] super::Version::V0 => { let versioned_request = v0::VersionedRequest::try_from(self.clone())?; - let (request, version) = versioned_request.extract()?; + let (extracted_v0_info, version) = versioned_request.extract()?; - let upgrade_request = super::latest::interface::RequestData::try_from(request)?; + let v1_info = v1::interface::RequestData::try_from(extracted_v0_info)?; + let v2_info = v2::interface::RequestData::try_from(v1_info)?; - Ok((upgrade_request.into(), version)) + let request_data = RequestData::from(v2_info); + Ok((request_data, version)) } super::Version::V1 => { - let versioned_request = v1::VersionedRequest::try_from(self.clone())?; - let (extracted, version) = versioned_request.extract()?; - Ok((extracted.into(), version)) + let versioned_request = v1::VersionedRequest::try_from(self)?; + let (extracted_v1_info, version) = versioned_request.extract()?; + let v2_info = v2::interface::RequestData::try_from(extracted_v1_info)?; + + let request_data = RequestData::from(v2_info); + Ok((request_data, version)) + } + super::Version::V2 => { + let versioned_request = v2::VersionedRequest::try_from(self)?; + let (extracted_v2_info, version) = versioned_request.extract()?; + + let request_data = RequestData::from(extracted_v2_info); + Ok((request_data, version)) } } } } pub enum ResponseData { - AvailableBandwidth(i64), - TopUpBandwidth(i64), + AvailableBandwidth { + amount: i64, + upgrade_mode: bool, + }, + TopUpBandwidth { + available_bandwidth: i64, + upgrade_mode: bool, + }, + UpgradeMode { + upgrade_mode: bool, + }, } impl Construct for Response { @@ -109,14 +172,26 @@ impl Construct for Response { match version { #[cfg(feature = "testing")] super::Version::V0 => { - let translate_response = super::latest::interface::ResponseData::from(info); - let downgrade_response = v0::interface::ResponseData::try_from(translate_response)?; - let versioned_response = - v0::VersionedResponse::construct(downgrade_response, version)?; + // attempt to go through conversion chain for `info`: v2 => v1 => v0 + let v2_info = v2::interface::ResponseData::from(info); + let v1_info = v1::interface::ResponseData::try_from(v2_info)?; + let v0_info = v0::interface::ResponseData::try_from(v1_info)?; + + let versioned_response = v0::VersionedResponse::construct(v0_info, version)?; Ok(versioned_response.try_into()?) } Version::V1 => { - let versioned_response = v1::VersionedResponse::construct(info.into(), version)?; + // attempt to go through conversion chain for `info`: v2 => v1 + let v2_info = v2::interface::ResponseData::from(info); + let v1_info = v1::interface::ResponseData::try_from(v2_info)?; + + let versioned_response = v1::VersionedResponse::construct(v1_info, version)?; + Ok(versioned_response.try_into()?) + } + Version::V2 => { + let v2_info = v2::interface::ResponseData::from(info); + + let versioned_response = v2::VersionedResponse::construct(v2_info, version)?; Ok(versioned_response.try_into()?) } } @@ -129,16 +204,27 @@ impl Extract for Response { #[cfg(feature = "testing")] super::Version::V0 => { let versioned_response = v0::VersionedResponse::try_from(self.clone())?; - let (response, version) = versioned_response.extract()?; + let (extracted_v0_info, version) = versioned_response.extract()?; + let v1_info = v1::interface::ResponseData::try_from(extracted_v0_info)?; + let v2_info = v2::interface::ResponseData::try_from(v1_info)?; - let upgrade_response = super::latest::interface::ResponseData::try_from(response)?; - - Ok((upgrade_response.into(), version)) + let response_data = ResponseData::from(v2_info); + Ok((response_data, version)) } super::Version::V1 => { let versioned_response = v1::VersionedResponse::try_from(self.clone())?; - let (extracted, version) = versioned_response.extract()?; - Ok((extracted.into(), version)) + let (extracted_v1_info, version) = versioned_response.extract()?; + let v2_info = v2::interface::ResponseData::try_from(extracted_v1_info)?; + + let response_data = ResponseData::from(v2_info); + Ok((response_data, version)) + } + super::Version::V2 => { + let versioned_response = v2::VersionedResponse::try_from(self.clone())?; + let (extracted_v2_info, version) = versioned_response.extract()?; + + let response_data = ResponseData::from(extracted_v2_info); + Ok((response_data, version)) } } } diff --git a/common/wireguard-private-metadata/shared/src/models/mod.rs b/common/wireguard-private-metadata/shared/src/models/mod.rs index e408d7c5df..ae39c890ac 100644 --- a/common/wireguard-private-metadata/shared/src/models/mod.rs +++ b/common/wireguard-private-metadata/shared/src/models/mod.rs @@ -14,7 +14,10 @@ pub mod interface; pub mod v0; // dummy version, only for filling boilerplate code for update/downgrade and testing pub mod v1; -pub use v1 as latest; +// adds upgrade mode information to bandwidth response +pub mod v2; + +pub use v2 as latest; use crate::models::error::Error; @@ -24,6 +27,7 @@ pub enum Version { /// only used for testing purposes, don't include it in your matching arms V0, V1, + V2, } impl From for Version { @@ -35,6 +39,7 @@ impl From for Version { match value { 0 => zero_version, 1 => Version::V1, + 2 => Version::V2, _ => latest::VERSION, // if unknown, it means we're behind, so we can use the latest we know about } } @@ -47,6 +52,7 @@ impl From for u64 { #[cfg(feature = "testing")] Version::V0 => 0, Version::V1 => 1, + Version::V2 => 2, } } } @@ -57,12 +63,24 @@ pub struct Request { pub(crate) inner: Vec, } -#[derive(Clone, Serialize, Deserialize, ToSchema)] +impl Request { + pub fn new(version: Version, inner: Vec) -> Self { + Request { version, inner } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct Response { pub version: Version, pub(crate) inner: Vec, } +impl Response { + pub fn new(version: Version, inner: Vec) -> Self { + Response { version, inner } + } +} + pub trait Extract { fn extract(&self) -> Result<(T, Version), Error>; } diff --git a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs index 78dfdec7b5..c9cef56089 100644 --- a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs +++ b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs @@ -1,66 +1,29 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bincode::Options; use serde::{Deserialize, Serialize}; -use crate::{make_bincode_serializer, models::Request}; - -use super::super::{Error, QueryType, VersionedRequest}; +use super::super::{QueryType, VersionedRequest}; +use crate::impl_default_bincode_request_query_conversions; #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct InnerAvailableBandwidthRequest {} -impl TryFrom for InnerAvailableBandwidthRequest { - type Error = Error; - - fn try_from(value: VersionedRequest) -> Result { - match value.query_type { - QueryType::AvailableBandwidth => { - Ok(make_bincode_serializer().deserialize(&value.inner)?) - } - QueryType::TopupBandwidth => Err(Error::InvalidQueryType { - source_query_type: value.query_type.to_string(), - target_query_type: QueryType::AvailableBandwidth.to_string(), - }), - } - } -} - -impl TryFrom for VersionedRequest { - type Error = Error; - - fn try_from(value: InnerAvailableBandwidthRequest) -> Result { - Ok(Self { - query_type: QueryType::AvailableBandwidth, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for InnerAvailableBandwidthRequest { - type Error = crate::error::MetadataError; - - fn try_from(value: Request) -> Result { - VersionedRequest::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} - -impl TryFrom for Request { - type Error = crate::error::MetadataError; - - fn try_from(value: InnerAvailableBandwidthRequest) -> Result { - VersionedRequest::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} +// Implements: +// - TryFrom<&VersionedRequest> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerAvailableBandwidthRequest +// - TryFrom for InnerAvailableBandwidthRequest +// - TryFrom<&InnerTopUpRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerAvailableBandwidthRequest, + QueryType::AvailableBandwidth, + QueryType::AvailableBandwidth +); #[cfg(test)] mod tests { diff --git a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs index 5243e312ee..d822a72e60 100644 --- a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs +++ b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs @@ -1,66 +1,30 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bincode::Options; use serde::{Deserialize, Serialize}; -use crate::{make_bincode_serializer, models::Response}; +use crate::impl_default_bincode_response_query_conversions; -use super::super::{Error, QueryType, VersionedResponse}; +use super::super::{QueryType, VersionedResponse}; #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct InnerAvailableBandwidthResponse {} -impl TryFrom for InnerAvailableBandwidthResponse { - type Error = Error; - - fn try_from(value: VersionedResponse) -> Result { - match value.query_type { - QueryType::AvailableBandwidth => { - Ok(make_bincode_serializer().deserialize(&value.inner)?) - } - QueryType::TopupBandwidth => Err(Error::InvalidQueryType { - source_query_type: value.query_type.to_string(), - target_query_type: QueryType::AvailableBandwidth.to_string(), - }), - } - } -} - -impl TryFrom for VersionedResponse { - type Error = Error; - - fn try_from(value: InnerAvailableBandwidthResponse) -> Result { - Ok(Self { - query_type: QueryType::AvailableBandwidth, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for InnerAvailableBandwidthResponse { - type Error = crate::error::MetadataError; - - fn try_from(value: Response) -> Result { - VersionedResponse::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} - -impl TryFrom for Response { - type Error = crate::error::MetadataError; - - fn try_from(value: InnerAvailableBandwidthResponse) -> Result { - VersionedResponse::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} +// Implements: +// - TryFrom<&VersionedResponse> for InnerAvailableBandwidthResponse +// - TryFrom for InnerAvailableBandwidthResponse +// - TryFrom<&InnerAvailableBandwidthResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerAvailableBandwidthResponse +// - TryFrom for InnerAvailableBandwidthResponse +// - TryFrom<&InnerAvailableBandwidthResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerAvailableBandwidthResponse, + QueryType::AvailableBandwidth, + QueryType::AvailableBandwidth +); #[cfg(test)] mod tests { diff --git a/common/wireguard-private-metadata/shared/src/models/v0/interface.rs b/common/wireguard-private-metadata/shared/src/models/v0/interface.rs index f52daba30e..9c6fc462b3 100644 --- a/common/wireguard-private-metadata/shared/src/models/v0/interface.rs +++ b/common/wireguard-private-metadata/shared/src/models/v0/interface.rs @@ -9,6 +9,7 @@ use super::{ topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, }; use crate::models::{Construct, Extract, Version, error::Error}; +use crate::{Request, Response}; #[derive(Debug, Clone, PartialEq)] pub enum RequestData { @@ -35,11 +36,11 @@ impl Extract for VersionedRequest { fn extract(&self) -> Result<(RequestData, Version), Error> { match self.query_type { QueryType::AvailableBandwidth => { - let _req = InnerAvailableBandwidthRequest::try_from(self.clone())?; + let _req = InnerAvailableBandwidthRequest::try_from(self)?; Ok((RequestData::AvailableBandwidth(()), VERSION)) } - QueryType::TopupBandwidth => { - let _req = InnerTopUpRequest::try_from(self.clone())?; + QueryType::TopUpBandwidth => { + let _req = InnerTopUpRequest::try_from(self)?; Ok((RequestData::TopUpBandwidth(()), VERSION)) } } @@ -61,13 +62,46 @@ impl Extract for VersionedResponse { fn extract(&self) -> Result<(ResponseData, Version), Error> { match self.query_type { QueryType::AvailableBandwidth => { - let _resp = InnerAvailableBandwidthResponse::try_from(self.clone())?; + let _resp = InnerAvailableBandwidthResponse::try_from(self)?; Ok((ResponseData::AvailableBandwidth(()), VERSION)) } - QueryType::TopupBandwidth => { - let _resp = InnerTopUpResponse::try_from(self.clone())?; + QueryType::TopUpBandwidth => { + let _resp = InnerTopUpResponse::try_from(self)?; Ok((ResponseData::TopUpBandwidth(()), VERSION)) } } } } + +#[cfg(feature = "testing")] +impl Extract for Request { + fn extract(&self) -> Result<(RequestData, Version), Error> { + match self.version { + Version::V0 => { + let versioned_request = VersionedRequest::try_from(self)?; + versioned_request.extract() + } + _ => Err(Error::UpdateNotPossible { + from: self.version, + to: VERSION, + }), + } + } +} + +#[cfg(feature = "testing")] +impl Construct for Response { + fn construct(info: ResponseData, version: Version) -> Result { + match version { + Version::V0 => { + let translate_response = info; + let versioned_response = VersionedResponse::construct(translate_response, version)?; + Ok(versioned_response.try_into()?) + } + _ => Err(Error::DowngradeNotPossible { + from: version, + to: VERSION, + }), + } + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/mod.rs b/common/wireguard-private-metadata/shared/src/models/v0/mod.rs index 997fbf6f57..fd90ac124a 100644 --- a/common/wireguard-private-metadata/shared/src/models/v0/mod.rs +++ b/common/wireguard-private-metadata/shared/src/models/v0/mod.rs @@ -3,15 +3,13 @@ use std::fmt::Display; -use bincode::Options; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use super::error::Error; use crate::{ - make_bincode_serializer, - models::{Request, Response, Version}, + impl_default_bincode_request_conversions, impl_default_bincode_response_conversions, + models::Version, }; pub(crate) mod available_bandwidth; @@ -31,7 +29,7 @@ pub use topup_bandwidth::{ #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] pub enum QueryType { AvailableBandwidth, - TopupBandwidth, + TopUpBandwidth, } impl Display for QueryType { @@ -45,62 +43,24 @@ pub struct VersionedRequest { query_type: QueryType, inner: Vec, } - -impl TryFrom for Request { - type Error = Error; - - fn try_from(value: VersionedRequest) -> Result { - Ok(Request { - version: VERSION, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for VersionedRequest { - type Error = Error; - - fn try_from(value: Request) -> Result { - if value.version != VERSION { - return Err(Error::InvalidVersion { - source_version: value.version, - target_version: VERSION, - }); - } - Ok(make_bincode_serializer().deserialize(&value.inner)?) - } -} +// Implements: +// - TryFrom<&VersionedRequest> for Request +// - TryFrom for Request +// - TryFrom<&Request> for VersionedRequest +// - TryFrom for VersionedRequest +impl_default_bincode_request_conversions!(VersionedRequest, VERSION); #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct VersionedResponse { query_type: QueryType, inner: Vec, } - -impl TryFrom for Response { - type Error = Error; - - fn try_from(value: VersionedResponse) -> Result { - Ok(Response { - version: VERSION, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for VersionedResponse { - type Error = Error; - - fn try_from(value: Response) -> Result { - if value.version != VERSION { - return Err(Error::InvalidVersion { - source_version: value.version, - target_version: VERSION, - }); - } - Ok(make_bincode_serializer().deserialize(&value.inner)?) - } -} +// Implements: +// - TryFrom<&VersionedResponse> for Response +// - TryFrom for Response +// - TryFrom<&Response> for VersionedResponse +// - TryFrom for VersionedResponse +impl_default_bincode_response_conversions!(VersionedResponse, VERSION); #[cfg(test)] mod tests { @@ -110,8 +70,10 @@ mod tests { }, topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, }; - use super::*; + use crate::make_bincode_serializer; + use crate::{Request, Response}; + use bincode::Options; #[test] fn serde_request_av_bw() { @@ -146,7 +108,7 @@ mod tests { #[test] fn serde_request_topup() { let req = VersionedRequest { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: make_bincode_serializer() .serialize(&InnerTopUpRequest {}) .unwrap(), @@ -161,7 +123,7 @@ mod tests { #[test] fn serde_response_topup() { let resp = VersionedResponse { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: make_bincode_serializer() .serialize(&InnerTopUpResponse {}) .unwrap(), diff --git a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs index 9c333478d2..3cb15b5ee8 100644 --- a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs +++ b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs @@ -1,64 +1,30 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bincode::Options; use serde::{Deserialize, Serialize}; -use crate::{make_bincode_serializer, models::Request}; +use crate::impl_default_bincode_request_query_conversions; -use super::super::{Error, QueryType, VersionedRequest}; +use super::super::{QueryType, VersionedRequest}; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct InnerTopUpRequest {} -impl TryFrom for InnerTopUpRequest { - type Error = Error; - - fn try_from(value: VersionedRequest) -> Result { - match value.query_type { - QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), - QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { - source_query_type: value.query_type.to_string(), - target_query_type: QueryType::TopupBandwidth.to_string(), - }), - } - } -} - -impl TryFrom for VersionedRequest { - type Error = Error; - - fn try_from(value: InnerTopUpRequest) -> Result { - Ok(Self { - query_type: QueryType::TopupBandwidth, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for InnerTopUpRequest { - type Error = crate::error::MetadataError; - - fn try_from(value: Request) -> Result { - VersionedRequest::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} - -impl TryFrom for Request { - type Error = crate::error::MetadataError; - - fn try_from(value: InnerTopUpRequest) -> Result { - VersionedRequest::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} +// Implements: +// - TryFrom<&VersionedRequest> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerTopUpRequest, + QueryType::TopUpBandwidth, + QueryType::TopUpBandwidth +); #[cfg(test)] mod tests { @@ -68,7 +34,7 @@ mod tests { fn serde() { let req = InnerTopUpRequest {}; let ser = VersionedRequest::try_from(req.clone()).unwrap(); - assert_eq!(QueryType::TopupBandwidth, ser.query_type); + assert_eq!(QueryType::TopUpBandwidth, ser.query_type); let de = InnerTopUpRequest::try_from(ser).unwrap(); assert_eq!(req, de); } @@ -76,7 +42,7 @@ mod tests { #[test] fn empty_content() { let future_req = VersionedRequest { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: vec![], }; assert!(InnerTopUpRequest::try_from(future_req).is_ok()); diff --git a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs index cd934b6e7e..346792ce73 100644 --- a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs +++ b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs @@ -1,64 +1,30 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bincode::Options; use serde::{Deserialize, Serialize}; -use crate::{make_bincode_serializer, models::Response}; +use crate::impl_default_bincode_response_query_conversions; -use super::super::{Error, QueryType, VersionedResponse}; +use super::super::{QueryType, VersionedResponse}; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct InnerTopUpResponse {} -impl TryFrom for InnerTopUpResponse { - type Error = Error; - - fn try_from(value: VersionedResponse) -> Result { - match value.query_type { - QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), - QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { - source_query_type: value.query_type.to_string(), - target_query_type: QueryType::TopupBandwidth.to_string(), - }), - } - } -} - -impl TryFrom for VersionedResponse { - type Error = Error; - - fn try_from(value: InnerTopUpResponse) -> Result { - Ok(Self { - query_type: QueryType::TopupBandwidth, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for InnerTopUpResponse { - type Error = crate::error::MetadataError; - - fn try_from(value: Response) -> Result { - VersionedResponse::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} - -impl TryFrom for Response { - type Error = crate::error::MetadataError; - - fn try_from(value: InnerTopUpResponse) -> Result { - VersionedResponse::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} +// Implements: +// - TryFrom<&VersionedResponse> for InnerTopUpResponse +// - TryFrom for InnerTopUpResponse +// - TryFrom<&InnerTopUpResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerTopUpResponse +// - TryFrom for InnerTopUpResponse +// - TryFrom<&InnerTopUpResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerTopUpResponse, + QueryType::TopUpBandwidth, + QueryType::TopUpBandwidth +); #[cfg(test)] mod tests { @@ -68,7 +34,7 @@ mod tests { fn serde() { let resp = InnerTopUpResponse {}; let ser = VersionedResponse::try_from(resp.clone()).unwrap(); - assert_eq!(QueryType::TopupBandwidth, ser.query_type); + assert_eq!(QueryType::TopUpBandwidth, ser.query_type); let de = InnerTopUpResponse::try_from(ser).unwrap(); assert_eq!(resp, de); } @@ -76,7 +42,7 @@ mod tests { #[test] fn empty_content() { let future_resp = VersionedResponse { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: vec![], }; assert!(InnerTopUpResponse::try_from(future_resp).is_ok()); diff --git a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs index 78dfdec7b5..702c06377a 100644 --- a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs +++ b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs @@ -1,66 +1,30 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bincode::Options; use serde::{Deserialize, Serialize}; -use crate::{make_bincode_serializer, models::Request}; +use crate::impl_default_bincode_request_query_conversions; -use super::super::{Error, QueryType, VersionedRequest}; +use super::super::{QueryType, VersionedRequest}; #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct InnerAvailableBandwidthRequest {} -impl TryFrom for InnerAvailableBandwidthRequest { - type Error = Error; - - fn try_from(value: VersionedRequest) -> Result { - match value.query_type { - QueryType::AvailableBandwidth => { - Ok(make_bincode_serializer().deserialize(&value.inner)?) - } - QueryType::TopupBandwidth => Err(Error::InvalidQueryType { - source_query_type: value.query_type.to_string(), - target_query_type: QueryType::AvailableBandwidth.to_string(), - }), - } - } -} - -impl TryFrom for VersionedRequest { - type Error = Error; - - fn try_from(value: InnerAvailableBandwidthRequest) -> Result { - Ok(Self { - query_type: QueryType::AvailableBandwidth, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for InnerAvailableBandwidthRequest { - type Error = crate::error::MetadataError; - - fn try_from(value: Request) -> Result { - VersionedRequest::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} - -impl TryFrom for Request { - type Error = crate::error::MetadataError; - - fn try_from(value: InnerAvailableBandwidthRequest) -> Result { - VersionedRequest::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} +// Implements: +// - TryFrom<&VersionedRequest> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerAvailableBandwidthRequest +// - TryFrom for InnerAvailableBandwidthRequest +// - TryFrom<&InnerTopUpRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerAvailableBandwidthRequest, + QueryType::AvailableBandwidth, + QueryType::AvailableBandwidth +); #[cfg(test)] mod tests { diff --git a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs index f5addd609f..a65ae64aed 100644 --- a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs +++ b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs @@ -1,68 +1,32 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bincode::Options; use serde::{Deserialize, Serialize}; -use crate::{make_bincode_serializer, models::Response}; +use crate::impl_default_bincode_response_query_conversions; -use super::super::{Error, QueryType, VersionedResponse}; +use super::super::{QueryType, VersionedResponse}; #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct InnerAvailableBandwidthResponse { pub available_bandwidth: i64, } -impl TryFrom for InnerAvailableBandwidthResponse { - type Error = Error; - - fn try_from(value: VersionedResponse) -> Result { - match value.query_type { - QueryType::AvailableBandwidth => { - Ok(make_bincode_serializer().deserialize(&value.inner)?) - } - QueryType::TopupBandwidth => Err(Error::InvalidQueryType { - source_query_type: value.query_type.to_string(), - target_query_type: QueryType::AvailableBandwidth.to_string(), - }), - } - } -} - -impl TryFrom for VersionedResponse { - type Error = Error; - - fn try_from(value: InnerAvailableBandwidthResponse) -> Result { - Ok(Self { - query_type: QueryType::AvailableBandwidth, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for InnerAvailableBandwidthResponse { - type Error = crate::error::MetadataError; - - fn try_from(value: Response) -> Result { - VersionedResponse::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} - -impl TryFrom for Response { - type Error = crate::error::MetadataError; - - fn try_from(value: InnerAvailableBandwidthResponse) -> Result { - VersionedResponse::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} +// Implements: +// - TryFrom<&VersionedResponse> for InnerAvailableBandwidthResponse +// - TryFrom for InnerAvailableBandwidthResponse +// - TryFrom<&InnerAvailableBandwidthResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerAvailableBandwidthResponse +// - TryFrom for InnerAvailableBandwidthResponse +// - TryFrom<&InnerAvailableBandwidthResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerAvailableBandwidthResponse, + QueryType::AvailableBandwidth, + QueryType::AvailableBandwidth +); #[cfg(test)] mod tests { diff --git a/common/wireguard-private-metadata/shared/src/models/v1/interface.rs b/common/wireguard-private-metadata/shared/src/models/v1/interface.rs index 763222e04a..7a02463956 100644 --- a/common/wireguard-private-metadata/shared/src/models/v1/interface.rs +++ b/common/wireguard-private-metadata/shared/src/models/v1/interface.rs @@ -5,6 +5,8 @@ use nym_credentials_interface::CredentialSpendingData; #[cfg(feature = "testing")] use super::super::v0 as previous; +#[cfg(feature = "testing")] +use crate::{Request, Response, v0}; use super::{ QueryType, VERSION, VersionedRequest, VersionedResponse, @@ -46,7 +48,7 @@ impl Extract for VersionedRequest { let _req = InnerAvailableBandwidthRequest::try_from(self.clone())?; Ok((RequestData::AvailableBandwidth(()), VERSION)) } - QueryType::TopupBandwidth => { + QueryType::TopUpBandwidth => { let req = InnerTopUpRequest::try_from(self.clone())?; Ok(( RequestData::TopUpBandwidth(Box::new(req.credential)), @@ -84,7 +86,7 @@ impl Extract for VersionedResponse { VERSION, )) } - QueryType::TopupBandwidth => { + QueryType::TopUpBandwidth => { let resp = InnerTopUpResponse::try_from(self.clone())?; Ok(( ResponseData::TopUpBandwidth(resp.available_bandwidth), @@ -98,7 +100,7 @@ impl Extract for VersionedResponse { // this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions #[cfg(feature = "testing")] impl TryFrom for RequestData { - type Error = super::Error; + type Error = crate::models::error::Error; fn try_from(value: previous::interface::RequestData) -> Result { match value { @@ -106,7 +108,7 @@ impl TryFrom for RequestData { Ok(Self::AvailableBandwidth(inner)) } previous::interface::RequestData::TopUpBandwidth(_) => { - Err(super::Error::UpdateNotPossible { + Err(crate::models::Error::UpdateNotPossible { from: previous::VERSION, to: VERSION, }) @@ -118,7 +120,7 @@ impl TryFrom for RequestData { // this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions #[cfg(feature = "testing")] impl TryFrom for previous::interface::RequestData { - type Error = super::Error; + type Error = crate::models::error::Error; fn try_from(value: RequestData) -> Result { match value { @@ -131,18 +133,18 @@ impl TryFrom for previous::interface::RequestData { // this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions #[cfg(feature = "testing")] impl TryFrom for ResponseData { - type Error = super::Error; + type Error = crate::models::error::Error; fn try_from(value: previous::interface::ResponseData) -> Result { match value { previous::interface::ResponseData::AvailableBandwidth(_) => { - Err(super::Error::UpdateNotPossible { + Err(crate::models::error::Error::UpdateNotPossible { from: previous::VERSION, to: VERSION, }) } previous::interface::ResponseData::TopUpBandwidth(_) => { - Err(super::Error::UpdateNotPossible { + Err(crate::models::error::Error::UpdateNotPossible { from: previous::VERSION, to: VERSION, }) @@ -154,7 +156,7 @@ impl TryFrom for ResponseData { // this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions #[cfg(feature = "testing")] impl TryFrom for previous::interface::ResponseData { - type Error = super::Error; + type Error = crate::models::error::Error; fn try_from(value: ResponseData) -> Result { match value { @@ -164,13 +166,64 @@ impl TryFrom for previous::interface::ResponseData { } } +#[cfg(feature = "testing")] +impl Extract for Request { + fn extract(&self) -> Result<(RequestData, Version), Error> { + match self.version { + Version::V0 => { + let versioned_request = v0::VersionedRequest::try_from(self)?; + let (extracted_v0_info, version) = versioned_request.extract()?; + + let v1_info = RequestData::try_from(extracted_v0_info)?; + Ok((v1_info, version)) + } + Version::V1 => { + let versioned_request = VersionedRequest::try_from(self)?; + versioned_request.extract() + } + // a v1 server does not have any code for downgrading v2 into v1 + _ => Err(Error::DowngradeNotPossible { + from: self.version, + to: VERSION, + }), + } + } +} + +#[cfg(feature = "testing")] +impl Construct for Response { + fn construct(info: ResponseData, version: Version) -> Result { + match version { + Version::V0 => { + let v1_info = info; + let v0_info = v0::interface::ResponseData::try_from(v1_info)?; + + let versioned_response = v0::VersionedResponse::construct(v0_info, version)?; + Ok(versioned_response.try_into()?) + } + Version::V1 => { + let translate_response = info; + let versioned_response = VersionedResponse::construct(translate_response, version)?; + Ok(versioned_response.try_into()?) + } + // a v1 server does not have any code for downgrading v2 into v1 + _ => Err(Error::DowngradeNotPossible { + from: version, + to: VERSION, + }), + } + } +} + #[cfg(test)] mod test { + #[cfg(feature = "testing")] + use super::*; + #[cfg(feature = "testing")] use crate::models::tests::CREDENTIAL_BYTES; - use super::*; - #[test] + #[cfg(feature = "testing")] fn request_upgrade() { assert_eq!( RequestData::try_from(previous::interface::RequestData::AvailableBandwidth(())) @@ -183,6 +236,7 @@ mod test { } #[test] + #[cfg(feature = "testing")] fn response_upgrade() { assert!( ResponseData::try_from(previous::interface::ResponseData::AvailableBandwidth(())) @@ -194,6 +248,7 @@ mod test { } #[test] + #[cfg(feature = "testing")] fn request_downgrade() { assert_eq!( previous::interface::RequestData::try_from(RequestData::AvailableBandwidth(())) @@ -210,6 +265,7 @@ mod test { } #[test] + #[cfg(feature = "testing")] fn response_downgrade() { assert_eq!( previous::interface::ResponseData::try_from(ResponseData::AvailableBandwidth(42)) diff --git a/common/wireguard-private-metadata/shared/src/models/v1/mod.rs b/common/wireguard-private-metadata/shared/src/models/v1/mod.rs index 787a74f671..050b97caa1 100644 --- a/common/wireguard-private-metadata/shared/src/models/v1/mod.rs +++ b/common/wireguard-private-metadata/shared/src/models/v1/mod.rs @@ -3,15 +3,13 @@ use std::fmt::Display; -use bincode::Options; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use super::error::Error; use crate::{ - make_bincode_serializer, - models::{Request, Response, Version}, + impl_default_bincode_request_conversions, impl_default_bincode_response_conversions, + models::Version, }; pub use available_bandwidth::{ @@ -31,7 +29,7 @@ pub const VERSION: Version = Version::V1; #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] pub enum QueryType { AvailableBandwidth, - TopupBandwidth, + TopUpBandwidth, } impl Display for QueryType { @@ -45,82 +43,44 @@ pub struct VersionedRequest { query_type: QueryType, inner: Vec, } - -impl TryFrom for Request { - type Error = Error; - - fn try_from(value: VersionedRequest) -> Result { - Ok(Request { - version: VERSION, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for VersionedRequest { - type Error = Error; - - fn try_from(value: Request) -> Result { - if value.version != VERSION { - return Err(Error::InvalidVersion { - source_version: value.version, - target_version: VERSION, - }); - } - Ok(make_bincode_serializer().deserialize(&value.inner)?) - } -} +// Implements: +// - TryFrom<&VersionedRequest> for Request +// - TryFrom for Request +// - TryFrom<&Request> for VersionedRequest +// - TryFrom for VersionedRequest +impl_default_bincode_request_conversions!(VersionedRequest, VERSION); #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct VersionedResponse { query_type: QueryType, inner: Vec, } - -impl TryFrom for Response { - type Error = Error; - - fn try_from(value: VersionedResponse) -> Result { - Ok(Response { - version: VERSION, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for VersionedResponse { - type Error = Error; - - fn try_from(value: Response) -> Result { - if value.version != VERSION { - return Err(Error::InvalidVersion { - source_version: value.version, - target_version: VERSION, - }); - } - Ok(make_bincode_serializer().deserialize(&value.inner)?) - } -} +// Implements: +// - TryFrom<&VersionedResponse> for Response +// - TryFrom for Response +// - TryFrom<&Response> for VersionedResponse +// - TryFrom for VersionedResponse +impl_default_bincode_response_conversions!(VersionedResponse, VERSION); #[cfg(test)] mod tests { - - use nym_credentials_interface::CredentialSpendingData; - - use crate::models::tests::CREDENTIAL_BYTES; - use self::{ available_bandwidth::{ request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, }, topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, }; + use crate::models::error::Error; + use crate::models::tests::CREDENTIAL_BYTES; + use crate::{Request, Response, make_bincode_serializer}; + use bincode::Options; + use nym_credentials_interface::CredentialSpendingData; use super::*; #[test] fn mismatched_request_version() { - let version = Version::V0; + let version = Version::V2; let future_bw = Request { version, inner: vec![], @@ -139,7 +99,7 @@ mod tests { #[test] fn mismatched_response_version() { - let version = Version::V0; + let version = Version::V2; let future_bw = Response { version, inner: vec![], @@ -191,7 +151,7 @@ mod tests { #[test] fn serde_request_topup() { let req = VersionedRequest { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: make_bincode_serializer() .serialize(&InnerTopUpRequest { credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), @@ -208,7 +168,7 @@ mod tests { #[test] fn serde_response_topup() { let resp = VersionedResponse { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: make_bincode_serializer() .serialize(&InnerTopUpResponse { available_bandwidth: 42, diff --git a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs index 871cc127ef..3a3de39689 100644 --- a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs +++ b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs @@ -1,13 +1,12 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bincode::Options; use nym_credentials_interface::CredentialSpendingData; use serde::{Deserialize, Serialize}; -use crate::{make_bincode_serializer, models::Request}; +use crate::impl_default_bincode_request_query_conversions; -use super::super::{Error, QueryType, VersionedRequest}; +use super::super::{QueryType, VersionedRequest}; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct InnerTopUpRequest { @@ -15,54 +14,21 @@ pub struct InnerTopUpRequest { pub credential: CredentialSpendingData, } -impl TryFrom for InnerTopUpRequest { - type Error = Error; - - fn try_from(value: VersionedRequest) -> Result { - match value.query_type { - QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), - QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { - source_query_type: value.query_type.to_string(), - target_query_type: QueryType::TopupBandwidth.to_string(), - }), - } - } -} - -impl TryFrom for VersionedRequest { - type Error = Error; - - fn try_from(value: InnerTopUpRequest) -> Result { - Ok(Self { - query_type: QueryType::TopupBandwidth, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for InnerTopUpRequest { - type Error = crate::error::MetadataError; - - fn try_from(value: Request) -> Result { - VersionedRequest::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} - -impl TryFrom for Request { - type Error = crate::error::MetadataError; - - fn try_from(value: InnerTopUpRequest) -> Result { - VersionedRequest::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} +// Implements: +// - TryFrom<&VersionedRequest> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerTopUpRequest, + QueryType::TopUpBandwidth, + QueryType::TopUpBandwidth +); #[cfg(test)] mod tests { @@ -76,7 +42,7 @@ mod tests { credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), }; let ser = VersionedRequest::try_from(req.clone()).unwrap(); - assert_eq!(QueryType::TopupBandwidth, ser.query_type); + assert_eq!(QueryType::TopUpBandwidth, ser.query_type); let de = InnerTopUpRequest::try_from(ser).unwrap(); assert_eq!(req, de); } @@ -84,7 +50,7 @@ mod tests { #[test] fn invalid_content() { let future_req = VersionedRequest { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: vec![], }; assert!(InnerTopUpRequest::try_from(future_req).is_err()); diff --git a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs index 08e0ef111f..dccc735b8f 100644 --- a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs +++ b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs @@ -1,66 +1,32 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bincode::Options; use serde::{Deserialize, Serialize}; -use crate::{make_bincode_serializer, models::Response}; +use crate::impl_default_bincode_response_query_conversions; -use super::super::{Error, QueryType, VersionedResponse}; +use super::super::{QueryType, VersionedResponse}; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct InnerTopUpResponse { pub available_bandwidth: i64, } -impl TryFrom for InnerTopUpResponse { - type Error = Error; - - fn try_from(value: VersionedResponse) -> Result { - match value.query_type { - QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), - QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { - source_query_type: value.query_type.to_string(), - target_query_type: QueryType::TopupBandwidth.to_string(), - }), - } - } -} - -impl TryFrom for VersionedResponse { - type Error = Error; - - fn try_from(value: InnerTopUpResponse) -> Result { - Ok(Self { - query_type: QueryType::TopupBandwidth, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for InnerTopUpResponse { - type Error = crate::error::MetadataError; - - fn try_from(value: Response) -> Result { - VersionedResponse::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} - -impl TryFrom for Response { - type Error = crate::error::MetadataError; - - fn try_from(value: InnerTopUpResponse) -> Result { - VersionedResponse::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} +// Implements: +// - TryFrom<&VersionedResponse> for InnerTopUpResponse +// - TryFrom for InnerTopUpResponse +// - TryFrom<&InnerTopUpResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerTopUpResponse +// - TryFrom for InnerTopUpResponse +// - TryFrom<&InnerTopUpResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerTopUpResponse, + QueryType::TopUpBandwidth, + QueryType::TopUpBandwidth +); #[cfg(test)] mod tests { @@ -72,7 +38,7 @@ mod tests { available_bandwidth: 42, }; let ser = VersionedResponse::try_from(resp.clone()).unwrap(); - assert_eq!(QueryType::TopupBandwidth, ser.query_type); + assert_eq!(QueryType::TopUpBandwidth, ser.query_type); let de = InnerTopUpResponse::try_from(ser).unwrap(); assert_eq!(resp, de); } @@ -80,7 +46,7 @@ mod tests { #[test] fn invalid_content() { let future_resp = VersionedResponse { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: vec![], }; assert!(InnerTopUpResponse::try_from(future_resp).is_err()); diff --git a/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/mod.rs new file mode 100644 index 0000000000..10698cca2d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/request.rs new file mode 100644 index 0000000000..702c06377a --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/request.rs @@ -0,0 +1,50 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +use crate::impl_default_bincode_request_query_conversions; + +use super::super::{QueryType, VersionedRequest}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthRequest {} + +// Implements: +// - TryFrom<&VersionedRequest> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerAvailableBandwidthRequest +// - TryFrom for InnerAvailableBandwidthRequest +// - TryFrom<&InnerTopUpRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerAvailableBandwidthRequest, + QueryType::AvailableBandwidth, + QueryType::AvailableBandwidth +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let req = InnerAvailableBandwidthRequest {}; + let ser = VersionedRequest::try_from(req).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn empty_content() { + let future_req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthRequest::try_from(future_req).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/response.rs new file mode 100644 index 0000000000..0da8eb0a2b --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/response.rs @@ -0,0 +1,56 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +use crate::impl_default_bincode_response_query_conversions; + +use super::super::{QueryType, VersionedResponse}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthResponse { + pub available_bandwidth: i64, + pub upgrade_mode: bool, +} + +// Implements: +// - TryFrom<&VersionedResponse> for InnerAvailableBandwidthResponse +// - TryFrom for InnerAvailableBandwidthResponse +// - TryFrom<&InnerAvailableBandwidthResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerAvailableBandwidthResponse +// - TryFrom for InnerAvailableBandwidthResponse +// - TryFrom<&InnerAvailableBandwidthResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerAvailableBandwidthResponse, + QueryType::AvailableBandwidth, + QueryType::AvailableBandwidth +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerAvailableBandwidthResponse { + available_bandwidth: 42, + upgrade_mode: false, + }; + let ser = VersionedResponse::try_from(resp).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn invalid_content() { + let future_resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthResponse::try_from(future_resp).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/mod.rs b/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/mod.rs new file mode 100644 index 0000000000..10698cca2d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/request.rs b/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/request.rs new file mode 100644 index 0000000000..2baba00f8a --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/request.rs @@ -0,0 +1,76 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +use crate::impl_default_bincode_request_query_conversions; + +use super::super::{QueryType, VersionedRequest}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum UpgradeModeCheckRequestType { + /// Attempt to request upgrade mode recheck via the JWT issued as the result of + /// global attestation.json being published + UpgradeModeJwt { token: String }, +} + +// each versioned variant should always be a subset of the latest one defined in the interface +// so a From trait should always be implementable (as opposed to having to do TryFrom) +// (but this is not applicable in this instance as this IS the latest (05.11.25) +// impl From for crate::models::interface::UpgradeModeCheckRequestType { +// fn from(typ: UpgradeModeCheckRequestType) -> Self { +// match typ { +// UpgradeModeCheckRequestType::UpgradeModeJwt { token } => { +// crate::models::interface::UpgradeModeCheckRequestType::UpgradeModeJwt { token } +// } +// } +// } +// } + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerUpgradeModeCheckRequest { + pub request_type: UpgradeModeCheckRequestType, +} + +// Implements: +// - TryFrom<&VersionedRequest> for InnerUpgradeModeCheckRequest +// - TryFrom for InnerUpgradeModeCheckRequest +// - TryFrom<&InnerUpgradeModeCheckRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerUpgradeModeCheckRequest +// - TryFrom for InnerUpgradeModeCheckRequest +// - TryFrom<&InnerUpgradeModeCheckRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerUpgradeModeCheckRequest, + QueryType::UpgradeModeCheck, + QueryType::UpgradeModeCheck +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let req = InnerUpgradeModeCheckRequest { + request_type: UpgradeModeCheckRequestType::UpgradeModeJwt { + token: "dummy.jwt.token".to_string(), + }, + }; + let ser = VersionedRequest::try_from(req.clone()).unwrap(); + assert_eq!(QueryType::UpgradeModeCheck, ser.query_type); + let de = InnerUpgradeModeCheckRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn invalid_content() { + let future_req = VersionedRequest { + query_type: QueryType::UpgradeModeCheck, + inner: vec![], + }; + assert!(InnerUpgradeModeCheckRequest::try_from(future_req).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/response.rs b/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/response.rs new file mode 100644 index 0000000000..3b83ae69eb --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/response.rs @@ -0,0 +1,52 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +use crate::impl_default_bincode_response_query_conversions; + +use super::super::{QueryType, VersionedResponse}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerUpgradeModeCheckResponse { + pub upgrade_mode: bool, +} + +// Implements: +// - TryFrom<&VersionedResponse> for InnerUpgradeModeCheckResponse +// - TryFrom for InnerUpgradeModeCheckResponse +// - TryFrom<&InnerUpgradeModeCheckResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerUpgradeModeCheckResponse +// - TryFrom for InnerUpgradeModeCheckResponse +// - TryFrom<&InnerUpgradeModeCheckResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerUpgradeModeCheckResponse, + QueryType::UpgradeModeCheck, + QueryType::UpgradeModeCheck +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerUpgradeModeCheckResponse { upgrade_mode: true }; + let ser = VersionedResponse::try_from(resp.clone()).unwrap(); + assert_eq!(QueryType::UpgradeModeCheck, ser.query_type); + let de = InnerUpgradeModeCheckResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn invalid_content() { + let future_resp = VersionedResponse { + query_type: QueryType::UpgradeModeCheck, + inner: vec![], + }; + assert!(InnerUpgradeModeCheckResponse::try_from(future_resp).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v2/interface.rs b/common/wireguard-private-metadata/shared/src/models/v2/interface.rs new file mode 100644 index 0000000000..9a5e79ff61 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/interface.rs @@ -0,0 +1,304 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::BandwidthCredential; + +use super::super::v1 as previous; + +use super::{ + QueryType, VERSION, VersionedRequest, VersionedResponse, + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + check_upgrade_mode::{ + request::{InnerUpgradeModeCheckRequest, UpgradeModeCheckRequestType}, + response::InnerUpgradeModeCheckResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, +}; +use crate::models::{Construct, Extract, Version, error::Error}; + +#[derive(Debug, Clone, PartialEq)] +pub enum RequestData { + AvailableBandwidth, + TopUpBandwidth { + credential: Box, + }, + UpgradeModeCheck { + typ: UpgradeModeCheckRequestType, + }, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ResponseData { + AvailableBandwidth { + amount: i64, + upgrade_mode: bool, + }, + TopUpBandwidth { + available_bandwidth: i64, + upgrade_mode: bool, + }, + UpgradeMode { + upgrade_mode: bool, + }, +} + +impl Construct for VersionedRequest { + fn construct(info: RequestData, _version: Version) -> Result { + match info { + RequestData::AvailableBandwidth => Ok(InnerAvailableBandwidthRequest {}.try_into()?), + RequestData::TopUpBandwidth { credential } => Ok(InnerTopUpRequest { + credential: *credential, + } + .try_into()?), + RequestData::UpgradeModeCheck { typ } => { + Ok(InnerUpgradeModeCheckRequest { request_type: typ }.try_into()?) + } + } + } +} + +impl Extract for VersionedRequest { + fn extract(&self) -> Result<(RequestData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let _req = InnerAvailableBandwidthRequest::try_from(self)?; + Ok((RequestData::AvailableBandwidth, VERSION)) + } + QueryType::TopUpBandwidth => { + let req = InnerTopUpRequest::try_from(self)?; + Ok(( + RequestData::TopUpBandwidth { + credential: Box::new(req.credential), + }, + VERSION, + )) + } + QueryType::UpgradeModeCheck => { + let req = InnerUpgradeModeCheckRequest::try_from(self)?; + Ok(( + RequestData::UpgradeModeCheck { + typ: req.request_type, + }, + VERSION, + )) + } + } + } +} + +impl Construct for VersionedResponse { + fn construct(info: ResponseData, _version: Version) -> Result { + match info { + ResponseData::AvailableBandwidth { + amount, + upgrade_mode, + } => Ok(InnerAvailableBandwidthResponse { + available_bandwidth: amount, + upgrade_mode, + } + .try_into()?), + ResponseData::TopUpBandwidth { + available_bandwidth, + upgrade_mode, + } => Ok(InnerTopUpResponse { + available_bandwidth, + upgrade_mode, + } + .try_into()?), + ResponseData::UpgradeMode { upgrade_mode } => { + Ok(InnerUpgradeModeCheckResponse { upgrade_mode }.try_into()?) + } + } + } +} + +impl Extract for VersionedResponse { + fn extract(&self) -> Result<(ResponseData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let resp = InnerAvailableBandwidthResponse::try_from(self)?; + Ok(( + ResponseData::AvailableBandwidth { + amount: resp.available_bandwidth, + upgrade_mode: resp.upgrade_mode, + }, + VERSION, + )) + } + QueryType::TopUpBandwidth => { + let resp = InnerTopUpResponse::try_from(self)?; + Ok(( + ResponseData::TopUpBandwidth { + available_bandwidth: resp.available_bandwidth, + upgrade_mode: resp.upgrade_mode, + }, + VERSION, + )) + } + QueryType::UpgradeModeCheck => { + let resp = InnerUpgradeModeCheckResponse::try_from(self)?; + Ok(( + ResponseData::UpgradeMode { + upgrade_mode: resp.upgrade_mode, + }, + VERSION, + )) + } + } + } +} + +impl TryFrom for RequestData { + type Error = super::Error; + + fn try_from(value: previous::interface::RequestData) -> Result { + match value { + previous::interface::RequestData::AvailableBandwidth(_) => Ok(Self::AvailableBandwidth), + previous::interface::RequestData::TopUpBandwidth(zk_nym) => Ok(Self::TopUpBandwidth { + credential: Box::new((*zk_nym).into()), + }), + } + } +} + +impl TryFrom for previous::interface::RequestData { + type Error = super::Error; + + fn try_from(value: RequestData) -> Result { + match value { + RequestData::AvailableBandwidth => Ok(Self::AvailableBandwidth(())), + RequestData::TopUpBandwidth { credential } => match *credential { + BandwidthCredential::ZkNym(zk_nym) => Ok(Self::TopUpBandwidth(zk_nym)), + BandwidthCredential::UpgradeModeJWT { .. } => { + Err(super::Error::DowngradeNotPossible { + from: VERSION, + to: previous::VERSION, + }) + } + }, + RequestData::UpgradeModeCheck { .. } => Err(super::Error::DowngradeNotPossible { + from: VERSION, + to: previous::VERSION, + }), + } + } +} + +impl TryFrom for ResponseData { + type Error = super::Error; + + fn try_from(value: previous::interface::ResponseData) -> Result { + match value { + previous::interface::ResponseData::AvailableBandwidth(_) => { + Err(super::Error::UpdateNotPossible { + from: previous::VERSION, + to: VERSION, + }) + } + previous::interface::ResponseData::TopUpBandwidth(_) => { + Err(super::Error::UpdateNotPossible { + from: previous::VERSION, + to: VERSION, + }) + } + } + } +} + +impl TryFrom for previous::interface::ResponseData { + type Error = super::Error; + + fn try_from(value: ResponseData) -> Result { + match value { + ResponseData::AvailableBandwidth { amount, .. } => Ok(Self::AvailableBandwidth(amount)), + ResponseData::TopUpBandwidth { + available_bandwidth, + .. + } => Ok(Self::TopUpBandwidth(available_bandwidth)), + ResponseData::UpgradeMode { .. } => Err(super::Error::DowngradeNotPossible { + from: VERSION, + to: previous::VERSION, + }), + } + } +} + +#[cfg(test)] +mod test { + use crate::models::tests::CREDENTIAL_BYTES; + use nym_credentials_interface::CredentialSpendingData; + + use super::*; + + #[test] + fn request_upgrade() { + assert_eq!( + RequestData::try_from(previous::interface::RequestData::AvailableBandwidth(())) + .unwrap(), + RequestData::AvailableBandwidth + ); + assert_eq!( + RequestData::try_from(previous::interface::RequestData::TopUpBandwidth(Box::new( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap() + ))) + .unwrap(), + RequestData::TopUpBandwidth { + credential: Box::new(BandwidthCredential::ZkNym(Box::new( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap() + ))), + } + ); + } + + #[test] + fn response_upgrade() { + assert!( + ResponseData::try_from(previous::interface::ResponseData::AvailableBandwidth(42)) + .is_err() + ); + assert!( + ResponseData::try_from(previous::interface::ResponseData::TopUpBandwidth(42)).is_err() + ); + } + + #[test] + fn request_downgrade() { + assert_eq!( + previous::interface::RequestData::try_from(RequestData::AvailableBandwidth).unwrap(), + previous::interface::RequestData::AvailableBandwidth(()) + ); + assert_eq!( + previous::interface::RequestData::try_from(RequestData::TopUpBandwidth { + credential: Box::new(BandwidthCredential::from( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap() + )) + }) + .unwrap(), + previous::interface::RequestData::TopUpBandwidth(Box::new( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap() + )) + ); + } + + #[test] + fn response_downgrade() { + assert_eq!( + previous::interface::ResponseData::try_from(ResponseData::AvailableBandwidth { + amount: 42, + upgrade_mode: true + }) + .unwrap(), + previous::interface::ResponseData::AvailableBandwidth(42) + ); + assert_eq!( + previous::interface::ResponseData::try_from(ResponseData::TopUpBandwidth { + available_bandwidth: 42, + upgrade_mode: true, + }) + .unwrap(), + previous::interface::ResponseData::TopUpBandwidth(42) + ); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v2/mod.rs b/common/wireguard-private-metadata/shared/src/models/v2/mod.rs new file mode 100644 index 0000000000..6eea7d0713 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/mod.rs @@ -0,0 +1,197 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Display; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use super::error::Error; +use crate::{ + impl_default_bincode_request_conversions, impl_default_bincode_response_conversions, + models::Version, +}; + +pub use available_bandwidth::{ + request::InnerAvailableBandwidthRequest as AvailableBandwidthRequest, + response::InnerAvailableBandwidthResponse as AvailableBandwidthResponse, +}; +pub use check_upgrade_mode::{ + request::{ + InnerUpgradeModeCheckRequest as UpgradeModeCheckRequest, UpgradeModeCheckRequestType, + }, + response::InnerUpgradeModeCheckResponse as UpgradeModeCheckResponse, +}; +pub use topup_bandwidth::{ + request::InnerTopUpRequest as TopUpRequest, response::InnerTopUpResponse as TopUpResponse, +}; + +pub(crate) mod available_bandwidth; +pub(crate) mod check_upgrade_mode; +pub mod interface; +pub(crate) mod topup_bandwidth; + +pub const VERSION: Version = Version::V2; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub enum QueryType { + AvailableBandwidth, + TopUpBandwidth, + UpgradeModeCheck, +} + +impl Display for QueryType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedRequest { + query_type: QueryType, + inner: Vec, +} +// Implements: +// - TryFrom<&VersionedRequest> for Request +// - TryFrom for Request +// - TryFrom<&Request> for VersionedRequest +// - TryFrom for VersionedRequest +impl_default_bincode_request_conversions!(VersionedRequest, VERSION); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedResponse { + query_type: QueryType, + inner: Vec, +} +// Implements: +// - TryFrom<&VersionedResponse> for Response +// - TryFrom for Response +// - TryFrom<&Response> for VersionedResponse +// - TryFrom for VersionedResponse +impl_default_bincode_response_conversions!(VersionedResponse, VERSION); + +#[cfg(test)] +mod tests { + + use self::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + }; + use crate::models::tests::CREDENTIAL_BYTES; + use crate::{Request, Response, make_bincode_serializer}; + use bincode::Options; + use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData}; + + use super::*; + + #[test] + fn mismatched_request_version() { + let version = Version::V1; + let future_bw = Request { + version, + inner: vec![], + }; + if let Err(Error::InvalidVersion { + source_version, + target_version, + }) = VersionedRequest::try_from(future_bw) + { + assert_eq!(source_version, version); + assert_eq!(target_version, VERSION); + } else { + panic!("failed"); + }; + } + + #[test] + fn mismatched_response_version() { + let version = Version::V1; + let future_bw = Response { + version, + inner: vec![], + }; + if let Err(Error::InvalidVersion { + source_version, + target_version, + }) = VersionedResponse::try_from(future_bw) + { + assert_eq!(source_version, version); + assert_eq!(target_version, VERSION); + } else { + panic!("failed"); + }; + } + + #[test] + fn serde_request_av_bw() { + let req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthResponse { + available_bandwidth: 42, + upgrade_mode: true, + }) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_av_bw() { + let resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthRequest {}) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn serde_request_topup() { + let req = VersionedRequest { + query_type: QueryType::TopUpBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpRequest { + credential: BandwidthCredential::from( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + ), + }) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_topup() { + let resp = VersionedResponse { + query_type: QueryType::TopUpBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpResponse { + available_bandwidth: 42, + upgrade_mode: true, + }) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/mod.rs new file mode 100644 index 0000000000..10698cca2d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/request.rs new file mode 100644 index 0000000000..a029b3920e --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/request.rs @@ -0,0 +1,61 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::BandwidthCredential; +use serde::{Deserialize, Serialize}; + +use crate::impl_default_bincode_request_query_conversions; + +use super::super::{QueryType, VersionedRequest}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpRequest { + /// Ecash credential + pub credential: BandwidthCredential, +} + +// Implements: +// - TryFrom<&VersionedRequest> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerTopUpRequest, + QueryType::TopUpBandwidth, + QueryType::TopUpBandwidth +); + +#[cfg(test)] +mod tests { + use crate::models::tests::CREDENTIAL_BYTES; + use nym_credentials_interface::CredentialSpendingData; + + use super::*; + + #[test] + fn serde() { + let req = InnerTopUpRequest { + credential: BandwidthCredential::from( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + ), + }; + let ser = VersionedRequest::try_from(req.clone()).unwrap(); + assert_eq!(QueryType::TopUpBandwidth, ser.query_type); + let de = InnerTopUpRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn invalid_content() { + let future_req = VersionedRequest { + query_type: QueryType::TopUpBandwidth, + inner: vec![], + }; + assert!(InnerTopUpRequest::try_from(future_req).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/response.rs new file mode 100644 index 0000000000..0cbf3b00d8 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/response.rs @@ -0,0 +1,56 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +use crate::impl_default_bincode_response_query_conversions; + +use super::super::{QueryType, VersionedResponse}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpResponse { + pub available_bandwidth: i64, + pub upgrade_mode: bool, +} + +// Implements: +// - TryFrom<&VersionedResponse> for InnerTopUpResponse +// - TryFrom for InnerTopUpResponse +// - TryFrom<&InnerTopUpResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerTopUpResponse +// - TryFrom for InnerTopUpResponse +// - TryFrom<&InnerTopUpResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerTopUpResponse, + QueryType::TopUpBandwidth, + QueryType::TopUpBandwidth +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerTopUpResponse { + available_bandwidth: 42, + upgrade_mode: true, + }; + let ser = VersionedResponse::try_from(resp.clone()).unwrap(); + assert_eq!(QueryType::TopUpBandwidth, ser.query_type); + let de = InnerTopUpResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn invalid_content() { + let future_resp = VersionedResponse { + query_type: QueryType::TopUpBandwidth, + inner: vec![], + }; + assert!(InnerTopUpResponse::try_from(future_resp).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/routes.rs b/common/wireguard-private-metadata/shared/src/routes.rs index bda615fe1c..2919908e5d 100644 --- a/common/wireguard-private-metadata/shared/src/routes.rs +++ b/common/wireguard-private-metadata/shared/src/routes.rs @@ -4,7 +4,9 @@ pub const V1_API_VERSION: &str = "v1"; pub const BANDWIDTH: &str = "bandwidth"; +pub const NETWORK: &str = "network"; pub const VERSION: &str = "version"; pub const AVAILABLE: &str = "available"; pub const TOPUP: &str = "topup"; +pub const UPGRADE_MODE_CHECK: &str = "upgrade-mode-check"; diff --git a/common/wireguard-private-metadata/tests/Cargo.toml b/common/wireguard-private-metadata/tests/Cargo.toml index 6827c16f60..074d088f15 100644 --- a/common/wireguard-private-metadata/tests/Cargo.toml +++ b/common/wireguard-private-metadata/tests/Cargo.toml @@ -11,16 +11,21 @@ license.workspace = true [dependencies] async-trait = { workspace = true } axum = { workspace = true, features = ["tokio", "macros"] } +futures = { workspace = true } nym-credential-verification = { path = "../../credential-verification" } nym-credentials-interface = { path = "../../credentials-interface" } +nym-crypto = { path = "../../crypto", features = ["asymmetric"] } nym-http-api-client = { path = "../../http-api-client" } nym-http-api-common = { path = "../../http-api-common", features = [ "middleware", "utoipa", "output", ] } +nym-upgrade-mode-check = { path = "../../upgrade-mode-check" } nym-wireguard = { path = "../../wireguard" } +time = { workspace = true, features = ["macros"] } tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } +tower = { workspace = true } tower-http = { workspace = true, features = [ "cors", "trace", @@ -37,5 +42,3 @@ nym-wireguard-private-metadata-shared = { path = "../shared", features = [ ] } nym-wireguard-private-metadata-server = { path = "../server" } -[lints] -workspace = true diff --git a/common/wireguard-private-metadata/tests/src/lib.rs b/common/wireguard-private-metadata/tests/src/lib.rs index 7c8cbec81d..6f3d5ca20f 100644 --- a/common/wireguard-private-metadata/tests/src/lib.rs +++ b/common/wireguard-private-metadata/tests/src/lib.rs @@ -1,20 +1,86 @@ #[cfg(test)] mod v0; +#[cfg(test)] +mod v1; +#[cfg(test)] +mod v2; + +// TODO: we might possibly want to move it to some common crate +// so that it could be re-used by other tests (if needed) +#[cfg(test)] +pub(crate) mod mock_connect_info; #[cfg(test)] mod tests { - use std::net::SocketAddr; - + use crate::v2::peer_controller::PeerControlRequestTypeV2; + use nym_credential_verification::upgrade_mode::UpgradeModeEnableError; use nym_credential_verification::{ClientBandwidth, TicketVerifier}; - use nym_credentials_interface::CredentialSpendingData; - use nym_http_api_client::Client; - use nym_wireguard::{CONTROL_CHANNEL_SIZE, peer_controller::PeerControlRequest}; - use nym_wireguard_private_metadata_client::WireguardMetadataApiClient; - use nym_wireguard_private_metadata_server::{ - AppState, PeerControllerTransceiver, RouterBuilder, + use nym_credentials_interface::{ + AvailableBandwidth, BandwidthCredential, CredentialSpendingData, }; - use nym_wireguard_private_metadata_shared::{latest, v0, v1}; - use tokio::{net::TcpListener, sync::mpsc}; + use nym_crypto::asymmetric::ed25519; + use nym_http_api_client::HttpClientError; + use nym_upgrade_mode_check::{ + CREDENTIAL_PROXY_JWT_ISSUER, UpgradeModeAttestation, + generate_jwt_for_upgrade_mode_attestation, generate_new_attestation_with_starting_time, + }; + use nym_wireguard_private_metadata_client::WireguardMetadataApiClient; + use nym_wireguard_private_metadata_shared::{v0, v1, v2}; + use std::net::IpAddr; + use std::time::Duration; + use time::OffsetDateTime; + use time::macros::datetime; + + fn unchecked_ip>(raw: S) -> IpAddr { + raw.into().parse().unwrap() + } + + const HIGH_BANDWIDTH: i64 = 20000000000000; + + const DUMMY_JWT_ISSUER_ED25519_PRIVATE_KEY: [u8; 32] = [ + 152, 17, 144, 255, 213, 219, 246, 208, 109, 33, 100, 73, 1, 141, 32, 63, 141, 89, 167, 2, + 52, 215, 241, 219, 200, 18, 159, 241, 76, 111, 42, 32, + ]; + + pub(crate) fn dummy_jwt_issuer_public_key() -> ed25519::PublicKey { + let private_key = + ed25519::PrivateKey::from_bytes(&DUMMY_JWT_ISSUER_ED25519_PRIVATE_KEY).unwrap(); + private_key.public_key() + } + + const DUMMY_ATTESTER_ED25519_PRIVATE_KEY: [u8; 32] = [ + 108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248, + 163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161, + ]; + + pub(crate) fn dummy_attester_public_key() -> ed25519::PublicKey { + let private_key = + ed25519::PrivateKey::from_bytes(&DUMMY_ATTESTER_ED25519_PRIVATE_KEY).unwrap(); + private_key.public_key() + } + + fn high_bandwidth() -> Result { + bandwidth_response(HIGH_BANDWIDTH) + } + + fn low_bandwidth() -> Result { + bandwidth_response(0) + } + + fn bandwidth_response(amount: i64) -> Result { + Ok::<_, nym_wireguard::Error>(ClientBandwidth::new(AvailableBandwidth { + bytes: amount, + expiration: OffsetDateTime::from_unix_timestamp(2000000000).unwrap(), + })) + } + + fn mock_verifier( + bandwidth: i64, + ) -> Result, nym_wireguard::Error> { + Ok::<_, nym_wireguard::Error>( + Box::new(MockVerifier::new(bandwidth)) as Box + ) + } pub(crate) const VERIFIER_AVAILABLE_BANDWIDTH: i64 = 42; pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [ @@ -82,6 +148,52 @@ mod tests { 0, 0, 0, 0, 0, 1, ]; + pub(crate) fn mock_upgrade_mode_attestation() -> UpgradeModeAttestation { + let starting_time = datetime!(2025-10-20 12:00 UTC); + + // just some random, HARDCODED, key + let key = ed25519::PrivateKey::from_bytes(&DUMMY_ATTESTER_ED25519_PRIVATE_KEY).unwrap(); + + generate_new_attestation_with_starting_time( + &key, + vec![dummy_jwt_issuer_public_key()], + starting_time, + ) + } + + pub(crate) fn mock_different_upgrade_mode_attestation() -> UpgradeModeAttestation { + let starting_time = datetime!(2025-10-30 12:00 UTC); + + // just some random, HARDCODED, key + let key = ed25519::PrivateKey::from_bytes(&[ + 108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248, + 163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161, + ]) + .unwrap(); + + generate_new_attestation_with_starting_time( + &key, + vec![dummy_jwt_issuer_public_key()], + starting_time, + ) + } + + pub(crate) fn mock_upgrade_mode_jwt() -> String { + let jwt_key = + ed25519::PrivateKey::from_bytes(&DUMMY_JWT_ISSUER_ED25519_PRIVATE_KEY).unwrap(); + let keys = ed25519::KeyPair::from(jwt_key); + // sanity check in case hardcoded values were modified inconsistently + debug_assert_eq!(*keys.public_key(), dummy_jwt_issuer_public_key()); + + let attestation = mock_upgrade_mode_attestation(); + generate_jwt_for_upgrade_mode_attestation( + attestation, + Duration::from_secs(60 * 60), + &keys, + Some(CREDENTIAL_PROXY_JWT_ISSUER), + ) + } + pub(crate) struct MockVerifier { ret: i64, } @@ -99,56 +211,12 @@ mod tests { } } - pub(crate) async fn spawn_server_and_create_client() -> Client { - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); - let router = RouterBuilder::with_default_routes() - .with_state(AppState::new(PeerControllerTransceiver::new(request_tx))) - .router; - - tokio::spawn(async move { - loop { - match request_rx.recv().await { - Some(PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx }) => { - response_tx - .send(Ok(ClientBandwidth::new(Default::default()))) - .ok(); - } - Some(PeerControlRequest::GetVerifierByIp { - ip: _, - credential: _, - response_tx, - }) => { - response_tx - .send(Ok(Box::new(MockVerifier::new( - VERIFIER_AVAILABLE_BANDWIDTH, - )))) - .ok(); - } - None => break, - _ => panic!("Not expected"), - } - } - }); - - tokio::spawn(async move { - axum::serve( - listener, - router.into_make_service_with_connect_info::(), - ) - .await - .unwrap(); - }); - Client::new_url(addr.to_string(), None).unwrap() - } - - #[tokio::test] - async fn query_latest_version() { - let client = spawn_server_and_create_client().await; - let version = client.version().await.unwrap(); - assert_eq!(version, latest::VERSION); - } + // #[tokio::test] + // async fn query_latest_version() { + // let client = super::v2::network::test::spawn_server_and_create_client().await; + // let version = client.version().await.unwrap(); + // assert_eq!(version, latest::VERSION); + // } #[tokio::test] async fn query_against_server_v0() { @@ -158,7 +226,7 @@ mod tests { let version = client.version().await.unwrap(); assert_eq!(version, v0::VERSION); - // v0 reqwests + // v0 requests let request = v0::AvailableBandwidthRequest {}.try_into().unwrap(); let response = client.available_bandwidth(&request).await.unwrap(); v0::AvailableBandwidthResponse::try_from(response).unwrap(); @@ -167,7 +235,7 @@ mod tests { let response = client.topup_bandwidth(&request).await.unwrap(); v0::TopUpResponse::try_from(response).unwrap(); - // v1 reqwests + // v1 requests let request = v1::AvailableBandwidthRequest {}.try_into().unwrap(); assert!(client.available_bandwidth(&request).await.is_err()); @@ -177,17 +245,30 @@ mod tests { .try_into() .unwrap(); assert!(client.topup_bandwidth(&request).await.is_err()); + + // v2 requests + let request = v2::AvailableBandwidthRequest {}.try_into().unwrap(); + assert!(client.available_bandwidth(&request).await.is_err()); + + let request = v2::TopUpRequest { + credential: BandwidthCredential::from( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + ), + } + .try_into() + .unwrap(); + assert!(client.topup_bandwidth(&request).await.is_err()); } #[tokio::test] async fn query_against_server_v1() { - let client = spawn_server_and_create_client().await; + let client = super::v1::network::test::spawn_server_and_create_client().await; // version check let version = client.version().await.unwrap(); assert_eq!(version, v1::VERSION); - // v0 reqwests + // v0 requests let request = v0::AvailableBandwidthRequest {}.try_into().unwrap(); let response = client.available_bandwidth(&request).await.unwrap(); v0::AvailableBandwidthResponse::try_from(response).unwrap(); @@ -195,7 +276,7 @@ mod tests { let request = v0::TopUpRequest {}.try_into().unwrap(); assert!(client.topup_bandwidth(&request).await.is_err()); - // v1 reqwests + // v1 requests let request = v1::AvailableBandwidthRequest {}.try_into().unwrap(); let response = client.available_bandwidth(&request).await.unwrap(); let available_bandwidth = v1::AvailableBandwidthResponse::try_from(response) @@ -213,5 +294,272 @@ mod tests { .unwrap() .available_bandwidth; assert_eq!(available_bandwidth, VERIFIER_AVAILABLE_BANDWIDTH); + + // v2 requests + let request = v2::AvailableBandwidthRequest {}.try_into().unwrap(); + assert!(client.available_bandwidth(&request).await.is_err()); + + let request = v2::TopUpRequest { + credential: BandwidthCredential::from( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + ), + } + .try_into() + .unwrap(); + assert!(client.topup_bandwidth(&request).await.is_err()); + } + + #[tokio::test] + async fn query_against_server_v2() { + let server_test = super::v2::network::test::spawn_server_and_create_client().await; + let client = &server_test.api_client; + + // version check + let version = client.version().await.unwrap(); + assert_eq!(version, v2::VERSION); + + // =========== + // v0 requests + // =========== + let client_ip = unchecked_ip("0.0.0.1"); + server_test.set_client_ip(client_ip); + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetClientBandwidthByIp { ip: client_ip }, + bandwidth_response(0), + ) + .await; + + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetVerifierByIp { ip: client_ip }, + mock_verifier(10), + ) + .await; + + let request = v0::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + v0::AvailableBandwidthResponse::try_from(response).unwrap(); + + let request = v0::TopUpRequest {}.try_into().unwrap(); + assert!(client.topup_bandwidth(&request).await.is_err()); + server_test.reset_registered_responses().await; + + // =========== + // v1 requests + // =========== + let client_ip = unchecked_ip("1.1.1.1"); + server_test.set_client_ip(client_ip); + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetClientBandwidthByIp { ip: client_ip }, + bandwidth_response(0), + ) + .await; + + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetVerifierByIp { ip: client_ip }, + mock_verifier(100), + ) + .await; + + let request = v1::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + let available_bandwidth = v1::AvailableBandwidthResponse::try_from(response) + .unwrap() + .available_bandwidth; + assert_eq!(available_bandwidth, 0); + + let request = v1::TopUpRequest { + credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + + let available_bandwidth = v1::TopUpResponse::try_from(response) + .unwrap() + .available_bandwidth; + assert_eq!(available_bandwidth, 100); + server_test.reset_registered_responses().await; + + // =========== + // v2 requests + // =========== + let client_ip = unchecked_ip("2.2.2.1"); + server_test.set_client_ip(client_ip); + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetClientBandwidthByIp { ip: client_ip }, + bandwidth_response(0), + ) + .await; + + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetVerifierByIp { ip: client_ip }, + mock_verifier(200), + ) + .await; + + let request = v2::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + let available = v2::AvailableBandwidthResponse::try_from(response).unwrap(); + assert_eq!(available.available_bandwidth, 0); + assert!(!available.upgrade_mode); + + let request = v2::TopUpRequest { + credential: BandwidthCredential::from( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + ), + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let top_up = v2::TopUpResponse::try_from(response).unwrap(); + assert_eq!(top_up.available_bandwidth, 200); + assert!(!top_up.upgrade_mode); + server_test.reset_registered_responses().await; + + // upgrade mode test + let upgrade_mode_client = unchecked_ip("2.2.2.2"); + server_test.set_client_ip(upgrade_mode_client); + let good_attestation_alt = mock_different_upgrade_mode_attestation(); + let good_jwt = mock_upgrade_mode_jwt(); + + // 1. send attestation when upgrade mode is not enabled + let request = v2::TopUpRequest { + credential: BandwidthCredential::UpgradeModeJWT { + token: good_jwt.clone(), + }, + } + .try_into() + .unwrap(); + let response_err = client.topup_bandwidth(&request).await.unwrap_err(); + let HttpClientError::EndpointFailure { error, .. } = response_err else { + panic!("unexpected response") + }; + assert!(error.contains(&UpgradeModeEnableError::AttestationNotPublished.to_string())); + server_test.reset_registered_responses().await; + + // 2.1. send attestation when upgrade mode is enabled (low bandwidth) + let request_typ = PeerControlRequestTypeV2::GetClientBandwidthByIp { + ip: upgrade_mode_client, + }; + server_test + .register_peer_controller_response(request_typ, low_bandwidth()) + .await; + server_test.enable_upgrade_mode().await; + let request = v2::TopUpRequest { + credential: BandwidthCredential::UpgradeModeJWT { + token: good_jwt.clone(), + }, + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let top_up = v2::TopUpResponse::try_from(response).unwrap(); + // as defined by `DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD` + assert_eq!(top_up.available_bandwidth, 1024 * 1024 * 1024); + assert!(top_up.upgrade_mode); + server_test.reset_registered_responses().await; + + // 2.2. send attestation when upgrade mode is enabled (high bandwidth) + let request_typ = PeerControlRequestTypeV2::GetClientBandwidthByIp { + ip: upgrade_mode_client, + }; + server_test + .register_peer_controller_response(request_typ, high_bandwidth()) + .await; + server_test.enable_upgrade_mode().await; + let request = v2::TopUpRequest { + credential: BandwidthCredential::UpgradeModeJWT { + token: good_jwt.clone(), + }, + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let top_up = v2::TopUpResponse::try_from(response).unwrap(); + assert_eq!(top_up.available_bandwidth, HIGH_BANDWIDTH); + assert!(top_up.upgrade_mode); + server_test.reset_registered_responses().await; + + // 3. send bad attestation when upgrade mode is enabled + // (we don't validate it, so client is let through) + // (the only case where invalid attestation would have been rejected is when server + // is not aware of the UM, and that was meant to trigger a refresh. however, a test for that + // is out of scope for these unit tests) + server_test + .change_upgrade_mode_attestation(good_attestation_alt) + .await; + server_test + .register_peer_controller_response(request_typ, high_bandwidth()) + .await; + let request = v2::TopUpRequest { + credential: BandwidthCredential::UpgradeModeJWT { + token: good_jwt.clone(), + }, + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let top_up = v2::TopUpResponse::try_from(response).unwrap(); + assert_eq!(top_up.available_bandwidth, HIGH_BANDWIDTH); + assert!(top_up.upgrade_mode); + server_test.reset_registered_responses().await; + + // 4. send zk-nym when upgrade mode is enabled + server_test + .register_peer_controller_response(request_typ, high_bandwidth()) + .await; + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetVerifierByIp { + ip: upgrade_mode_client, + }, + mock_verifier(300), + ) + .await; + let request = v2::TopUpRequest { + credential: BandwidthCredential::from( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + ), + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let top_up = v2::TopUpResponse::try_from(response).unwrap(); + // as defined by `DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD` + assert_eq!(top_up.available_bandwidth, 1024 * 1024 * 1024); + assert!(top_up.upgrade_mode); + server_test.reset_registered_responses().await; + + // attempt to enable UM with a valid token + // no global attestation + server_test.disable_upgrade_mode().await; + let request = v2::UpgradeModeCheckRequest { + request_type: v2::UpgradeModeCheckRequestType::UpgradeModeJwt { + token: "".to_string(), + }, + } + .try_into() + .unwrap(); + let response = client.request_upgrade_mode_check(&request).await; + assert!(response.is_err()); + + server_test.publish_upgrade_mode_attestation().await; + // global attestation + let request = v2::UpgradeModeCheckRequest { + request_type: v2::UpgradeModeCheckRequestType::UpgradeModeJwt { + token: mock_upgrade_mode_jwt(), + }, + } + .try_into() + .unwrap(); + let response = client.request_upgrade_mode_check(&request).await.unwrap(); + let upgrade_mode = v2::UpgradeModeCheckResponse::try_from(response).unwrap(); + assert!(upgrade_mode.upgrade_mode); } } diff --git a/common/wireguard-private-metadata/tests/src/mock_connect_info.rs b/common/wireguard-private-metadata/tests/src/mock_connect_info.rs new file mode 100644 index 0000000000..95633d9e01 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/mock_connect_info.rs @@ -0,0 +1,121 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use axum::extract::FromRequestParts; +use axum::http::Request; +use axum::http::request::Parts; +use std::fmt::Display; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::sync::Arc; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::task::{Context, Poll}; +use tower::Layer; +use tower::Service; + +#[derive(Clone)] +pub struct DummyConnectInfo { + // store it as atomic i32 to avoid having to use locks to read and set the value + address: Arc, +} + +impl Display for DummyConnectInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.address().fmt(f) + } +} + +impl DummyConnectInfo { + pub fn new() -> Self { + let dummy_ip = Ipv4Addr::new(1, 2, 3, 4); + DummyConnectInfo { + address: Arc::new(AtomicU32::new(dummy_ip.to_bits())), + } + } + + #[allow(clippy::panic)] + pub fn set(&self, address: IpAddr) { + let IpAddr::V4(v4_address) = address else { + // it would be relatively easy to support ipv6 with multiple atomics, + // but I didn't feel it was needed at the time + panic!("ipv6 not supported") + }; + + self.address.store(v4_address.to_bits(), Ordering::Relaxed); + } + + pub fn address(&self) -> SocketAddr { + let bits = self.address.load(Ordering::Relaxed); + let ipv4 = Ipv4Addr::from(bits); + + SocketAddr::new(IpAddr::V4(ipv4), 1791) + } + + pub fn ip(&self) -> IpAddr { + self.address().ip() + } +} + +#[async_trait] +impl FromRequestParts for DummyConnectInfo +where + S: Send + Sync, +{ + type Rejection = std::convert::Infallible; + + #[allow(clippy::panic)] + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + if let Some(info) = parts.extensions.get::() { + Ok(info.clone()) + } else { + // this is a test code so that's fine + panic!("DummyConnectInfo not set") + } + } +} + +#[derive(Clone)] +pub struct MockConnectInfoLayer { + info: DummyConnectInfo, +} + +impl MockConnectInfoLayer { + pub fn new(info: DummyConnectInfo) -> Self { + Self { info } + } +} + +impl Layer for MockConnectInfoLayer { + type Service = MockConnectInfoMiddleware; + + fn layer(&self, inner: S) -> Self::Service { + MockConnectInfoMiddleware { + inner, + info: self.info.clone(), + } + } +} + +#[derive(Clone)] +pub struct MockConnectInfoMiddleware { + inner: S, + info: DummyConnectInfo, +} + +impl Service> for MockConnectInfoMiddleware +where + S: Service>, +{ + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, mut req: Request) -> Self::Future { + req.extensions_mut().insert(self.info.clone()); + self.inner.call(req) + } +} diff --git a/common/wireguard-private-metadata/tests/src/v0/app_state.rs b/common/wireguard-private-metadata/tests/src/v0/app_state.rs new file mode 100644 index 0000000000..45222bda15 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v0/app_state.rs @@ -0,0 +1,7 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::v1::app_state::AppStateV1; + +// there have been no changes between v0 and v1 so there's no point in redefining it +pub type AppStateV0 = AppStateV1; diff --git a/common/wireguard-private-metadata/tests/src/v0/interface.rs b/common/wireguard-private-metadata/tests/src/v0/interface.rs deleted file mode 100644 index a09ff4307a..0000000000 --- a/common/wireguard-private-metadata/tests/src/v0/interface.rs +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use nym_wireguard_private_metadata_shared::{ - Construct, Extract, Request, Response, Version, v0 as latest, -}; - -pub enum RequestData { - AvailableBandwidth(()), - TopUpBandwidth(()), -} - -impl From for RequestData { - fn from(value: latest::interface::RequestData) -> Self { - match value { - latest::interface::RequestData::AvailableBandwidth(inner) => { - Self::AvailableBandwidth(inner) - } - latest::interface::RequestData::TopUpBandwidth(credential_spending_data) => { - Self::TopUpBandwidth(credential_spending_data) - } - } - } -} - -impl From for latest::interface::RequestData { - fn from(value: RequestData) -> Self { - match value { - RequestData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), - RequestData::TopUpBandwidth(credential_spending_data) => { - Self::TopUpBandwidth(credential_spending_data) - } - } - } -} - -impl From for ResponseData { - fn from(value: latest::interface::ResponseData) -> Self { - match value { - latest::interface::ResponseData::AvailableBandwidth(inner) => { - Self::AvailableBandwidth(inner) - } - latest::interface::ResponseData::TopUpBandwidth(credential_spending_data) => { - Self::TopUpBandwidth(credential_spending_data) - } - } - } -} - -impl From for latest::interface::ResponseData { - fn from(value: ResponseData) -> Self { - match value { - ResponseData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), - ResponseData::TopUpBandwidth(credential_spending_data) => { - Self::TopUpBandwidth(credential_spending_data) - } - } - } -} - -impl Construct for Request { - fn construct( - info: RequestData, - version: Version, - ) -> Result { - match version { - Version::V0 => { - let translate_info = latest::interface::RequestData::from(info); - let versioned_request = - latest::VersionedRequest::construct(translate_info, latest::VERSION)?; - Ok(versioned_request.try_into()?) - } - _ => Err( - nym_wireguard_private_metadata_shared::ModelError::DowngradeNotPossible { - from: version, - to: Version::V0, - }, - ), - } - } -} - -impl Extract for Request { - fn extract( - &self, - ) -> Result<(RequestData, Version), nym_wireguard_private_metadata_shared::ModelError> { - match self.version { - Version::V0 => { - let versioned_request = latest::VersionedRequest::try_from(self.clone())?; - let (request, version) = versioned_request.extract()?; - - Ok((request.into(), version)) - } - _ => Err( - nym_wireguard_private_metadata_shared::ModelError::UpdateNotPossible { - from: self.version, - to: Version::V0, - }, - ), - } - } -} - -pub enum ResponseData { - AvailableBandwidth(()), - TopUpBandwidth(()), -} - -impl Construct for Response { - fn construct( - info: ResponseData, - version: Version, - ) -> Result { - match version { - Version::V0 => { - let translate_response = latest::interface::ResponseData::from(info); - let versioned_response = - latest::VersionedResponse::construct(translate_response, version)?; - Ok(versioned_response.try_into()?) - } - _ => Err( - nym_wireguard_private_metadata_shared::ModelError::DowngradeNotPossible { - from: version, - to: Version::V0, - }, - ), - } - } -} - -impl Extract for Response { - fn extract( - &self, - ) -> Result<(ResponseData, Version), nym_wireguard_private_metadata_shared::ModelError> { - match self.version { - Version::V0 => { - let versioned_response = latest::VersionedResponse::try_from(self.clone())?; - let (response, version) = versioned_response.extract()?; - - Ok((response.into(), version)) - } - _ => Err( - nym_wireguard_private_metadata_shared::ModelError::UpdateNotPossible { - from: self.version, - to: Version::V0, - }, - ), - } - } -} diff --git a/common/wireguard-private-metadata/tests/src/v0/mod.rs b/common/wireguard-private-metadata/tests/src/v0/mod.rs index 7338cc5ae2..2af556497b 100644 --- a/common/wireguard-private-metadata/tests/src/v0/mod.rs +++ b/common/wireguard-private-metadata/tests/src/v0/mod.rs @@ -1,2 +1,3 @@ -pub(crate) mod interface; +pub(crate) mod app_state; pub(crate) mod network; +pub(crate) mod peer_controller; diff --git a/common/wireguard-private-metadata/tests/src/v0/network.rs b/common/wireguard-private-metadata/tests/src/v0/network.rs index b0b7361e84..e45c506b94 100644 --- a/common/wireguard-private-metadata/tests/src/v0/network.rs +++ b/common/wireguard-private-metadata/tests/src/v0/network.rs @@ -5,25 +5,24 @@ pub(crate) mod test { use std::net::SocketAddr; - use crate::{ - tests::{MockVerifier, VERIFIER_AVAILABLE_BANDWIDTH}, - v0::interface::{RequestData, ResponseData}, - }; + use crate::tests::{MockVerifier, VERIFIER_AVAILABLE_BANDWIDTH}; use axum::{Json, Router, extract::Query}; use nym_credential_verification::ClientBandwidth; use nym_http_api_client::Client; use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_wireguard::{CONTROL_CHANNEL_SIZE, peer_controller::PeerControlRequest}; use nym_wireguard_private_metadata_server::PeerControllerTransceiver; + use nym_wireguard_private_metadata_shared::v0::interface::{RequestData, ResponseData}; use nym_wireguard_private_metadata_shared::{ AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, v0 as latest, }; + use tokio::sync::mpsc::Receiver; use tokio::{net::TcpListener, sync::mpsc}; use tower_http::compression::CompressionLayer; - use nym_wireguard_private_metadata_server::AppState; + use crate::v0::app_state::AppStateV0; - fn bandwidth_routes() -> Router { + fn bandwidth_routes() -> Router { Router::new() .route("/version", axum::routing::get(version)) .route("/available", axum::routing::post(available_bandwidth)) @@ -31,32 +30,11 @@ pub(crate) mod test { .layer(CompressionLayer::new()) } - #[utoipa::path( - tag = "bandwidth", - get, - path = "/v1/bandwidth/version", - responses( - (status = 200, content( - (Response = "application/bincode") - )) - ), -)] async fn version(Query(output): Query) -> AxumResult> { let output = output.output.unwrap_or_default(); Ok(output.to_response(latest::VERSION.into())) } - #[utoipa::path( - tag = "bandwidth", - post, - request_body = Request, - path = "/v1/bandwidth/available", - responses( - (status = 200, content( - (Response = "application/bincode") - )) - ), -)] async fn available_bandwidth( Query(output): Query, Json(request): Json, @@ -74,17 +52,6 @@ pub(crate) mod test { Ok(output.to_response(response)) } - #[utoipa::path( - tag = "bandwidth", - post, - request_body = Request, - path = "/v1/bandwidth/topup", - responses( - (status = 200, content( - (Response = "application/bincode") - )) - ), -)] async fn topup_bandwidth( Query(output): Query, Json(request): Json, @@ -102,35 +69,41 @@ pub(crate) mod test { Ok(output.to_response(response)) } + fn spawn_mock_peer_controller(mut request_rx: Receiver) { + tokio::spawn(async move { + while let Some(request) = request_rx.recv().await { + match request { + PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx } => { + response_tx + .send(Ok(ClientBandwidth::new(Default::default()))) + .ok(); + } + PeerControlRequest::GetVerifierByIp { + ip: _, + credential: _, + response_tx, + } => { + response_tx + .send(Ok(Box::new(MockVerifier::new( + VERIFIER_AVAILABLE_BANDWIDTH, + )))) + .ok(); + } + _ => panic!("Not expected"), + } + } + }); + } + pub(crate) async fn spawn_server_and_create_client() -> Client { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); - let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let (request_tx, request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); let router = Router::new() .nest("/v1", Router::new().nest("/bandwidth", bandwidth_routes())) - .with_state(AppState::new(PeerControllerTransceiver::new(request_tx))); + .with_state(AppStateV0::new(PeerControllerTransceiver::new(request_tx))); - tokio::spawn(async move { - match request_rx.recv().await.unwrap() { - PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx } => { - response_tx - .send(Ok(ClientBandwidth::new(Default::default()))) - .ok(); - } - PeerControlRequest::GetVerifierByIp { - ip: _, - credential: _, - response_tx, - } => { - response_tx - .send(Ok(Box::new(MockVerifier::new( - VERIFIER_AVAILABLE_BANDWIDTH, - )))) - .ok(); - } - _ => panic!("Not expected"), - } - }); + spawn_mock_peer_controller(request_rx); tokio::spawn(async move { axum::serve( diff --git a/common/wireguard-private-metadata/tests/src/v0/peer_controller.rs b/common/wireguard-private-metadata/tests/src/v0/peer_controller.rs new file mode 100644 index 0000000000..b1f80ad87c --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v0/peer_controller.rs @@ -0,0 +1,9 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![allow(dead_code)] + +use crate::v2::peer_controller::{MockPeerControllerStateV2, MockPeerControllerV2}; + +pub type MockPeerControllerStateV0 = MockPeerControllerStateV2; +pub type MockPeerControllerV0 = MockPeerControllerV2; diff --git a/common/wireguard-private-metadata/tests/src/v1/app_state.rs b/common/wireguard-private-metadata/tests/src/v1/app_state.rs new file mode 100644 index 0000000000..f773165819 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v1/app_state.rs @@ -0,0 +1,33 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; +use nym_wireguard_private_metadata_server::PeerControllerTransceiver; +use nym_wireguard_private_metadata_shared::error::MetadataError; +use std::net::IpAddr; + +#[derive(Clone, axum::extract::FromRef)] +pub struct AppStateV1 { + transceiver: PeerControllerTransceiver, +} + +impl AppStateV1 { + pub fn new(transceiver: PeerControllerTransceiver) -> Self { + Self { transceiver } + } + + pub async fn available_bandwidth(&self, ip: IpAddr) -> Result { + self.transceiver.query_bandwidth(ip).await + } + + // Top up with a credential and return the afterwards available bandwidth + pub async fn topup_bandwidth( + &self, + ip: IpAddr, + credential: CredentialSpendingData, + ) -> Result { + self.transceiver + .topup_bandwidth(ip, Box::new(credential)) + .await + } +} diff --git a/common/wireguard-private-metadata/tests/src/v1/mod.rs b/common/wireguard-private-metadata/tests/src/v1/mod.rs new file mode 100644 index 0000000000..3cf9729866 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v1/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod app_state; +pub(crate) mod network; +pub(crate) mod peer_controller; diff --git a/common/wireguard-private-metadata/tests/src/v1/network.rs b/common/wireguard-private-metadata/tests/src/v1/network.rs new file mode 100644 index 0000000000..07b23fccbc --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v1/network.rs @@ -0,0 +1,134 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +pub(crate) mod test { + use std::net::SocketAddr; + + use crate::tests::{MockVerifier, VERIFIER_AVAILABLE_BANDWIDTH}; + use crate::v1::app_state::AppStateV1; + use axum::extract::{ConnectInfo, State}; + use axum::{Json, Router, extract::Query}; + use nym_credential_verification::ClientBandwidth; + use nym_http_api_client::Client; + use nym_http_api_common::{FormattedResponse, OutputParams}; + use nym_wireguard::{CONTROL_CHANNEL_SIZE, peer_controller::PeerControlRequest}; + use nym_wireguard_private_metadata_server::PeerControllerTransceiver; + use nym_wireguard_private_metadata_shared::v1::interface::{RequestData, ResponseData}; + use nym_wireguard_private_metadata_shared::{ + AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, v1 as latest, + }; + use tokio::sync::mpsc::Receiver; + use tokio::{net::TcpListener, sync::mpsc}; + use tower_http::compression::CompressionLayer; + + fn bandwidth_routes() -> Router { + Router::new() + .route("/version", axum::routing::get(version)) + .route("/available", axum::routing::post(available_bandwidth)) + .route("/topup", axum::routing::post(topup_bandwidth)) + .layer(CompressionLayer::new()) + } + + async fn version(Query(output): Query) -> AxumResult> { + let output = output.output.unwrap_or_default(); + Ok(output.to_response(latest::VERSION.into())) + } + + async fn available_bandwidth( + ConnectInfo(addr): ConnectInfo, + Query(output): Query, + State(state): State, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::AvailableBandwidth(_), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let available_bandwidth = state + .available_bandwidth(addr.ip()) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct( + ResponseData::AvailableBandwidth(available_bandwidth), + version, + ) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + async fn topup_bandwidth( + ConnectInfo(addr): ConnectInfo, + Query(output): Query, + State(state): State, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::TopUpBandwidth(credential), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let available_bandwidth = state + .topup_bandwidth(addr.ip(), *credential) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = + Response::construct(ResponseData::TopUpBandwidth(available_bandwidth), version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + fn spawn_mock_peer_controller(mut request_rx: Receiver) { + tokio::spawn(async move { + while let Some(request) = request_rx.recv().await { + match request { + PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx } => { + response_tx + .send(Ok(ClientBandwidth::new(Default::default()))) + .ok(); + } + PeerControlRequest::GetVerifierByIp { + ip: _, + credential: _, + response_tx, + } => { + response_tx + .send(Ok(Box::new(MockVerifier::new( + VERIFIER_AVAILABLE_BANDWIDTH, + )))) + .ok(); + } + _ => panic!("Not expected"), + } + } + }); + } + + pub(crate) async fn spawn_server_and_create_client() -> Client { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let (request_tx, request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let router = Router::new() + .nest("/v1", Router::new().nest("/bandwidth", bandwidth_routes())) + .with_state(AppStateV1::new(PeerControllerTransceiver::new(request_tx))); + + spawn_mock_peer_controller(request_rx); + + tokio::spawn(async move { + axum::serve( + listener, + router.into_make_service_with_connect_info::(), + ) + .await + .unwrap(); + }); + Client::new_url(addr.to_string(), None).unwrap() + } +} diff --git a/common/wireguard-private-metadata/tests/src/v1/peer_controller.rs b/common/wireguard-private-metadata/tests/src/v1/peer_controller.rs new file mode 100644 index 0000000000..7291d5c5b3 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v1/peer_controller.rs @@ -0,0 +1,9 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![allow(dead_code)] + +use crate::v2::peer_controller::{MockPeerControllerStateV2, MockPeerControllerV2}; + +pub type MockPeerControllerStateV1 = MockPeerControllerStateV2; +pub type MockPeerControllerV1 = MockPeerControllerV2; diff --git a/common/wireguard-private-metadata/tests/src/v2/app_state.rs b/common/wireguard-private-metadata/tests/src/v2/app_state.rs new file mode 100644 index 0000000000..3ebf886df1 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v2/app_state.rs @@ -0,0 +1,8 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_wireguard_private_metadata_server::AppState; + +// given latest is v2, we just create a type alias +// for any future versions, this would have to be adjusted +pub type AppStateV2 = AppState; diff --git a/common/wireguard-private-metadata/tests/src/v2/mod.rs b/common/wireguard-private-metadata/tests/src/v2/mod.rs new file mode 100644 index 0000000000..3cf9729866 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v2/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod app_state; +pub(crate) mod network; +pub(crate) mod peer_controller; diff --git a/common/wireguard-private-metadata/tests/src/v2/network.rs b/common/wireguard-private-metadata/tests/src/v2/network.rs new file mode 100644 index 0000000000..fd2520e8e5 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v2/network.rs @@ -0,0 +1,329 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +pub(crate) mod test { + use crate::mock_connect_info::{DummyConnectInfo, MockConnectInfoLayer}; + use crate::tests::{dummy_attester_public_key, mock_upgrade_mode_attestation}; + use crate::v2::app_state::AppStateV2; + use crate::v2::peer_controller::{ + MockPeerControllerStateV2, MockPeerControllerV2, PeerControlRequestTypeV2, + }; + use axum::extract::State; + use axum::{Extension, Json, Router, extract::Query}; + use futures::StreamExt; + use nym_credential_verification::upgrade_mode::{ + CheckRequest, UpgradeModeCheckConfig, UpgradeModeCheckRequestReceiver, + UpgradeModeCheckRequestSender, UpgradeModeDetails, UpgradeModeState, + }; + use nym_http_api_client::Client; + use nym_http_api_common::{FormattedResponse, OutputParams}; + use nym_upgrade_mode_check::UpgradeModeAttestation; + use nym_wireguard::CONTROL_CHANNEL_SIZE; + use nym_wireguard_private_metadata_server::AppState; + use nym_wireguard_private_metadata_server::PeerControllerTransceiver; + use nym_wireguard_private_metadata_shared::interface::RequestData; + use nym_wireguard_private_metadata_shared::{ + AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, v2 as latest, + }; + use std::any::Any; + use std::net::IpAddr; + use std::sync::Arc; + use std::time::Duration; + use tokio::sync::Mutex; + use tokio::task::JoinSet; + use tokio::{net::TcpListener, sync::mpsc}; + use tower_http::compression::CompressionLayer; + + pub struct MockUpgradeModeWatcher { + check_request_receiver: UpgradeModeCheckRequestReceiver, + upgrade_mode_state: UpgradeModeState, + + mock_published_attestation: Arc>>, + } + + impl MockUpgradeModeWatcher { + pub fn new( + check_request_receiver: UpgradeModeCheckRequestReceiver, + upgrade_mode_state: UpgradeModeState, + mock_published_attestation: Arc>>, + ) -> Self { + MockUpgradeModeWatcher { + check_request_receiver, + upgrade_mode_state, + mock_published_attestation, + } + } + + async fn handle_check_request(&mut self, polled_request: CheckRequest) { + let mut requests = vec![polled_request]; + while let Ok(Some(queued_up)) = self.check_request_receiver.try_next() { + requests.push(queued_up); + } + + let published = self.mock_published_attestation.lock().await; + self.upgrade_mode_state + .try_set_expected_attestation(published.clone()) + .await; + + for request in requests { + request.finalize() + } + } + + pub async fn run(&mut self) { + // for now don't do anything apart from notifying the caller + while let Some(polled_request) = self.check_request_receiver.next().await { + self.handle_check_request(polled_request).await + } + } + } + + pub struct ServerTest { + // among other things gives you access to the shared state, so you could toggle the flag + // and thus change server behaviour + upgrade_mode_state: UpgradeModeState, + + // shared state with the mock attestation watcher to make it think new attestation has been published + mock_published_attestation: Arc>>, + + connect_info: DummyConnectInfo, + + // handles to the following tasks: + // - the actual axum server + // - dummy attestation watcher + // - dummy peer controller + _server_tasks: JoinSet<()>, + + peer_controller_state: MockPeerControllerStateV2, + + pub(crate) api_client: Client, + } + + impl ServerTest { + pub(crate) async fn new() -> Self { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let (request_tx, request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + + let (um_recheck_tx, um_recheck_rx) = futures::channel::mpsc::unbounded(); + let upgrade_mode_state = UpgradeModeState::new(dummy_attester_public_key()); + let upgrade_mode_details = UpgradeModeDetails::new( + UpgradeModeCheckConfig { + // essentially we never want to trigger this in our tests + min_staleness_recheck: Duration::from_nanos(1), + }, + UpgradeModeCheckRequestSender::new(um_recheck_tx), + upgrade_mode_state.clone(), + ); + + let dummy_connect_info = DummyConnectInfo::new(); + + let router = Router::new() + .nest( + "/v1", + Router::new() + .nest("/bandwidth", bandwidth_routes()) + .nest("/network", network_routes()), + ) + .with_state(AppStateV2::new( + PeerControllerTransceiver::new(request_tx), + upgrade_mode_details, + )); + + // register responses for expected requests + let peer_controller_state = MockPeerControllerStateV2::default(); + let mut server_tasks = JoinSet::new(); + + let mut peer_controller = + MockPeerControllerV2::new(peer_controller_state.clone(), request_rx); + + let mock_published_attestation = Arc::new(Mutex::new(None)); + let mut upgrade_mode_watcher = MockUpgradeModeWatcher::new( + um_recheck_rx, + upgrade_mode_state.clone(), + mock_published_attestation.clone(), + ); + + // spawn all the tasks + server_tasks.spawn(async move { + peer_controller.run().await; + }); + server_tasks.spawn(async move { + upgrade_mode_watcher.run().await; + }); + + let connect_info = dummy_connect_info.clone(); + server_tasks.spawn(async move { + axum::serve( + listener, + // router.into_make_service_with_connect_info::(), + router.layer(MockConnectInfoLayer::new(connect_info)), + ) + .await + .unwrap(); + }); + let api_client = Client::new_url(addr.to_string(), None).unwrap(); + + ServerTest { + upgrade_mode_state, + mock_published_attestation, + connect_info: dummy_connect_info, + _server_tasks: server_tasks, + peer_controller_state, + api_client, + } + } + + pub(crate) async fn enable_upgrade_mode(&self) { + self.change_upgrade_mode_attestation(mock_upgrade_mode_attestation()) + .await + } + + pub(crate) async fn change_upgrade_mode_attestation( + &self, + attestation: UpgradeModeAttestation, + ) { + self.upgrade_mode_state + .try_set_expected_attestation(Some(attestation)) + .await + } + + pub(crate) async fn publish_upgrade_mode_attestation(&self) { + *self.mock_published_attestation.lock().await = Some(mock_upgrade_mode_attestation()) + } + + #[allow(dead_code)] + pub(crate) async fn disable_upgrade_mode(&self) { + self.upgrade_mode_state + .try_set_expected_attestation(None) + .await; + } + + pub(crate) fn set_client_ip(&self, ip: IpAddr) { + self.connect_info.set(ip) + } + + #[allow(dead_code)] + pub(crate) fn client_ip(&self) -> IpAddr { + self.connect_info.ip() + } + + // note: it's caller's responsibility to make sure the response type is correct! + pub(crate) async fn register_peer_controller_response( + &self, + request: PeerControlRequestTypeV2, + response: impl Any + Send + Sync + 'static, + ) { + self.peer_controller_state + .register_response(request, response) + .await + } + + pub(crate) async fn reset_registered_responses(&self) { + self.peer_controller_state + .clear_registered_responses() + .await + } + } + + fn bandwidth_routes() -> Router { + Router::new() + .route("/version", axum::routing::get(version)) + .route("/available", axum::routing::post(available_bandwidth)) + .route("/topup", axum::routing::post(topup_bandwidth)) + .layer(CompressionLayer::new()) + } + + fn network_routes() -> Router { + Router::new() + .route( + "/upgrade-mode-check", + axum::routing::post(upgrade_mode_check), + ) + .layer(CompressionLayer::new()) + } + + async fn version(Query(output): Query) -> AxumResult> { + let output = output.output.unwrap_or_default(); + Ok(output.to_response(latest::VERSION.into())) + } + + async fn available_bandwidth( + // ❗ \/ DIFFERENT FROM ACTUAL SERVER \/ ❗ + // we use different ConnectInfo to be able to mock different ip addresses + Extension(addr): Extension, + // ❗ /\ DIFFERENT FROM ACTUAL SERVER /\ ❗ + Query(output): Query, + State(state): State, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::AvailableBandwidth, version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let available_bandwidth_response = state + .available_bandwidth(addr.ip()) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct(available_bandwidth_response, version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + async fn topup_bandwidth( + // ❗ \/ DIFFERENT FROM ACTUAL SERVER \/ ❗ + // we use different ConnectInfo to be able to mock different ip addresses + Extension(addr): Extension, + // ❗ /\ DIFFERENT FROM ACTUAL SERVER /\ ❗ + Query(output): Query, + State(state): State, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::TopUpBandwidth { credential }, version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let top_up_bandwidth_response = state + .topup_bandwidth(addr.ip(), credential) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct(top_up_bandwidth_response, version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + async fn upgrade_mode_check( + Query(output): Query, + State(state): State, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::UpgradeModeCheck { typ }, version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let upgrade_mode_check_response = state + .upgrade_mode_check(typ) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct(upgrade_mode_check_response, version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + pub(crate) async fn spawn_server_and_create_client() -> ServerTest { + ServerTest::new().await + } +} diff --git a/common/wireguard-private-metadata/tests/src/v2/peer_controller.rs b/common/wireguard-private-metadata/tests/src/v2/peer_controller.rs new file mode 100644 index 0000000000..435359efac --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v2/peer_controller.rs @@ -0,0 +1,177 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// not declared as a 'global' since I can imagine it might change between versions + +use nym_wireguard::peer_controller::PeerControlRequest; +use std::any::Any; +use std::collections::{HashMap, VecDeque}; +use std::net::IpAddr; +use std::sync::Arc; +use tokio::sync::RwLock; +use tokio::sync::mpsc::Receiver; + +#[derive(Hash, PartialOrd, PartialEq, Clone, Debug, Eq, Copy)] +pub enum PeerControlRequestTypeV2 { + AddPeer, + RemovePeer, + QueryPeer, + GetClientBandwidthByKey, + GetClientBandwidthByIp { ip: IpAddr }, + GetVerifierByKey, + GetVerifierByIp { ip: IpAddr }, +} + +impl From<&PeerControlRequest> for PeerControlRequestTypeV2 { + fn from(req: &PeerControlRequest) -> Self { + match req { + PeerControlRequest::AddPeer { .. } => PeerControlRequestTypeV2::AddPeer, + PeerControlRequest::RemovePeer { .. } => PeerControlRequestTypeV2::RemovePeer, + PeerControlRequest::QueryPeer { .. } => PeerControlRequestTypeV2::QueryPeer, + PeerControlRequest::GetClientBandwidthByKey { .. } => { + PeerControlRequestTypeV2::GetClientBandwidthByKey + } + PeerControlRequest::GetClientBandwidthByIp { ip, .. } => { + PeerControlRequestTypeV2::GetClientBandwidthByIp { ip: *ip } + } + PeerControlRequest::GetVerifierByKey { .. } => { + PeerControlRequestTypeV2::GetVerifierByKey + } + PeerControlRequest::GetVerifierByIp { ip, .. } => { + PeerControlRequestTypeV2::GetVerifierByIp { ip: *ip } + } + } + } +} + +// all responses are registered as a queue for particular type +// (this is because the actual type can't be cloned as the `Error` does not implement Clone) +type RegisteredResponses = + HashMap>>; + +#[derive(Clone, Default)] +pub struct MockPeerControllerStateV2 { + registered_responses: Arc>, +} + +impl MockPeerControllerStateV2 { + pub async fn register_response( + &self, + request: PeerControlRequestTypeV2, + response: impl Any + Send + Sync + 'static, + ) { + self.registered_responses + .write() + .await + .entry(request) + .or_default() + .push_back(Box::new(response)); + } + + pub async fn clear_registered_responses(&self) { + self.registered_responses.write().await.clear(); + } +} + +pub struct MockPeerControllerV2 { + state: MockPeerControllerStateV2, + request_rx: Receiver, +} + +impl MockPeerControllerV2 { + pub(crate) fn new( + state: MockPeerControllerStateV2, + request_rx: Receiver, + ) -> Self { + MockPeerControllerV2 { state, request_rx } + } + + async fn handle_request(&mut self, request: PeerControlRequest) { + let typ = PeerControlRequestTypeV2::from(&request); + + let mut guard = self.state.registered_responses.write().await; + let Some(registered_responses) = guard.get_mut(&typ) else { + panic!( + "received a request for {typ:?} but there are no registered responses - this is probably due to a bug in your test setup" + ); + }; + + let Some(response) = registered_responses.pop_front() else { + panic!( + "received a request for {typ:?} but there are no registered responses - this is probably due to a bug in your test setup" + ); + }; + + match request { + PeerControlRequest::AddPeer { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .unwrap(); + } + PeerControlRequest::RemovePeer { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .unwrap(); + } + PeerControlRequest::QueryPeer { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .unwrap(); + } + PeerControlRequest::GetClientBandwidthByKey { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .unwrap(); + } + PeerControlRequest::GetClientBandwidthByIp { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .unwrap(); + } + PeerControlRequest::GetVerifierByKey { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .ok(); + } + PeerControlRequest::GetVerifierByIp { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .ok(); + } + } + } + + pub(crate) async fn run(&mut self) { + while let Some(request) = self.request_rx.recv().await { + self.handle_request(request).await; + } + } +} diff --git a/common/wireguard-types/Cargo.toml b/common/wireguard-types/Cargo.toml index e1ffda761c..9a8a783db0 100644 --- a/common/wireguard-types/Cargo.toml +++ b/common/wireguard-types/Cargo.toml @@ -12,14 +12,11 @@ license.workspace = true [dependencies] base64 = { workspace = true } -log = { workspace = true } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } -nym-config = { path = "../config" } -nym-network-defaults = { path = "../network-defaults" } - x25519-dalek = { workspace = true, features = ["static_secrets"] } +nym-crypto = { path = "../crypto", features = ["asymmetric"] } [dev-dependencies] rand = { workspace = true } diff --git a/common/wireguard-types/src/public_key.rs b/common/wireguard-types/src/public_key.rs index 3b3bbd60d1..755c60d628 100644 --- a/common/wireguard-types/src/public_key.rs +++ b/common/wireguard-types/src/public_key.rs @@ -9,11 +9,24 @@ use std::fmt; use std::ops::Deref; use std::str::FromStr; +use nym_crypto::asymmetric::x25519; use x25519_dalek::PublicKey; #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] pub struct PeerPublicKey(PublicKey); +impl From for PeerPublicKey { + fn from(pk: x25519::PublicKey) -> Self { + PeerPublicKey(pk.into()) + } +} + +impl From<&x25519::PublicKey> for PeerPublicKey { + fn from(pk: &x25519::PublicKey) -> Self { + (*pk).into() + } +} + impl PeerPublicKey { pub fn new(key: PublicKey) -> Self { PeerPublicKey(key) diff --git a/common/wireguard/Cargo.toml b/common/wireguard/Cargo.toml index e98e5fc27d..f2a773d4ec 100644 --- a/common/wireguard/Cargo.toml +++ b/common/wireguard/Cargo.toml @@ -11,27 +11,15 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -async-trait = { workspace = true } base64 = { workspace = true } -bincode = { workspace = true } -chrono = { workspace = true } -dashmap = { workspace = true } defguard_wireguard_rs = { workspace = true } -dyn-clone = { workspace = true } futures = { workspace = true } -# The latest version on crates.io at the time of writing this (6.0.0) has a -# version mismatch with x25519-dalek/curve25519-dalek that is resolved in the -# latest commit. So pick that for now. -x25519-dalek = { workspace = true } ip_network = { workspace = true } -log.workspace = true thiserror = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } tokio-stream = { workspace = true } -time = { workspace = true } tracing = { workspace = true } -nym-authenticator-requests = { path = "../authenticator-requests" } nym-credentials-interface = { path = "../credentials-interface" } nym-credential-verification = { path = "../credential-verification" } nym-crypto = { path = "../crypto", features = ["asymmetric"] } @@ -48,3 +36,6 @@ nym-gateway-storage = { path = "../gateway-storage", features = ["mock"] } [features] default = [] mock = ["nym-gateway-storage/mock"] + +[lints] +workspace = true diff --git a/common/wireguard/src/lib.rs b/common/wireguard/src/lib.rs index 6b2c632d22..cf7ff7f32f 100644 --- a/common/wireguard/src/lib.rs +++ b/common/wireguard/src/lib.rs @@ -7,13 +7,15 @@ // #![warn(clippy::unwrap_used)] use defguard_wireguard_rs::{WGApi, WireguardInterfaceApi, host::Peer, key::Key, net::IpAddrMask}; -#[cfg(target_os = "linux")] -use nym_credential_verification::ecash::EcashManager; use nym_crypto::asymmetric::x25519::KeyPair; use nym_wireguard_types::Config; use peer_controller::PeerControlRequest; use std::sync::Arc; use tokio::sync::mpsc::{self, Receiver, Sender}; +use tracing::error; + +#[cfg(target_os = "linux")] +use nym_credential_verification::ecash::EcashManager; #[cfg(target_os = "linux")] use nym_network_defaults::constants::WG_TUN_BASE_NAME; @@ -23,6 +25,8 @@ pub mod peer_controller; pub mod peer_handle; pub mod peer_storage_manager; +pub use error::Error; + pub const CONTROL_CHANNEL_SIZE: usize = 256; pub struct WgApiWrapper { @@ -114,7 +118,7 @@ impl Drop for WgApiWrapper { fn drop(&mut self) { if let Err(e) = defguard_wireguard_rs::WireguardInterfaceApi::remove_interface(&self.inner) { - log::error!("Could not remove the wireguard interface: {e:?}"); + error!("Could not remove the wireguard interface: {e:?}"); } } } @@ -163,6 +167,7 @@ pub async fn start_wireguard( ecash_manager: Arc, metrics: nym_node_metrics::NymNodeMetrics, peers: Vec, + upgrade_mode_status: nym_credential_verification::upgrade_mode::UpgradeModeStatus, shutdown_token: nym_task::ShutdownToken, wireguard_data: WireguardData, ) -> Result, Box> { @@ -250,6 +255,7 @@ pub async fn start_wireguard( peer_bandwidth_managers, wireguard_data.inner.peer_tx.clone(), wireguard_data.peer_rx, + upgrade_mode_status, shutdown_token, ); tokio::spawn(async move { controller.run().await }); diff --git a/common/wireguard/src/peer_controller.rs b/common/wireguard/src/peer_controller.rs index e9e4f78f2d..54b208c5af 100644 --- a/common/wireguard/src/peer_controller.rs +++ b/common/wireguard/src/peer_controller.rs @@ -1,13 +1,18 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::{ + error::{Error, Result}, + peer_handle::SharedBandwidthStorageManager, +}; +use crate::{peer_handle::PeerHandle, peer_storage_manager::CachedPeerManager}; use defguard_wireguard_rs::{ WireguardInterfaceApi, host::{Host, Peer}, key::Key, }; use futures::channel::oneshot; -use log::info; +use nym_credential_verification::upgrade_mode::UpgradeModeStatus; use nym_credential_verification::{ BandwidthFlushingBehaviourConfig, ClientBandwidth, CredentialVerifier, TicketVerifier, bandwidth_storage_manager::BandwidthStorageManager, ecash::traits::EcashManager, @@ -24,12 +29,7 @@ use std::{ }; use tokio::sync::{RwLock, mpsc}; use tokio_stream::{StreamExt, wrappers::IntervalStream}; - -use crate::{ - error::{Error, Result}, - peer_handle::SharedBandwidthStorageManager, -}; -use crate::{peer_handle::PeerHandle, peer_storage_manager::CachedPeerManager}; +use tracing::{debug, error, info, trace}; pub enum PeerControlRequest { AddPeer { @@ -84,6 +84,10 @@ pub struct PeerController { host_information: Arc>, bw_storage_managers: HashMap, timeout_check_interval: IntervalStream, + + /// Flag indicating whether the system is undergoing an upgrade and thus peers shouldn't be getting + /// their bandwidth metered. + upgrade_mode: UpgradeModeStatus, shutdown_token: nym_task::ShutdownToken, } @@ -97,6 +101,7 @@ impl PeerController { bw_storage_managers: HashMap, request_tx: mpsc::Sender, request_rx: mpsc::Receiver, + upgrade_mode: UpgradeModeStatus, shutdown_token: nym_task::ShutdownToken, ) -> Self { let timeout_check_interval = @@ -110,12 +115,13 @@ impl PeerController { cached_peer_manager, bandwidth_storage_manager.clone(), request_tx.clone(), + upgrade_mode.clone(), &shutdown_token, ); let public_key = public_key.clone(); tokio::spawn(async move { handle.run().await; - log::debug!("Peer handle shut down for {public_key}"); + debug!("Peer handle shut down for {public_key}"); }); } let bw_storage_managers = bw_storage_managers @@ -131,6 +137,7 @@ impl PeerController { request_tx, request_rx, timeout_check_interval, + upgrade_mode, shutdown_token, metrics, } @@ -145,7 +152,7 @@ impl PeerController { self.bw_storage_managers.remove(key); let ret = self.wg_api.remove_peer(key); if ret.is_err() { - log::error!( + error!( "Wireguard peer could not be removed from wireguard kernel module. Process should be restarted so that the interface is reset." ); } @@ -192,6 +199,7 @@ impl PeerController { cached_peer_manager, bandwidth_storage_manager.clone(), self.request_tx.clone(), + self.upgrade_mode.clone(), &self.shutdown_token, ); self.bw_storage_managers @@ -203,7 +211,7 @@ impl PeerController { let public_key = peer.public_key.clone(); tokio::spawn(async move { handle.run().await; - log::debug!("Peer handle shut down for {public_key}"); + debug!("Peer handle shut down for {public_key}"); }); Ok(()) } @@ -357,6 +365,7 @@ impl PeerController { } } + #[allow(clippy::expect_used)] self.metrics.wireguard.update( // if the conversion fails it means we're running not running on a 64bit system // and that's a reason enough for this failure. @@ -377,7 +386,7 @@ impl PeerController { tokio::select! { _ = self.timeout_check_interval.next() => { let Ok(host) = self.wg_api.read_interface_data() else { - log::error!("Can't read wireguard kernel data"); + error!("Can't read wireguard kernel data"); continue; }; self.update_metrics(&host).await; @@ -385,7 +394,7 @@ impl PeerController { *self.host_information.write().await = host; } _ = self.shutdown_token.cancelled() => { - log::trace!("PeerController handler: Received shutdown"); + trace!("PeerController handler: Received shutdown"); break; } msg = self.request_rx.recv() => { @@ -412,7 +421,7 @@ impl PeerController { response_tx.send(self.handle_query_verifier_by_ip(ip, *credential).await).ok(); } None => { - log::trace!("PeerController [main loop]: stopping since channel closed"); + trace!("PeerController [main loop]: stopping since channel closed"); break; } } @@ -429,6 +438,9 @@ struct MockWgApi { } #[cfg(feature = "mock")] +// unwraps, etc. are fine in test code +#[allow(clippy::unwrap_used)] +#[allow(clippy::todo)] impl WireguardInterfaceApi for MockWgApi { fn create_interface( &self, @@ -534,6 +546,7 @@ pub fn start_controller( Default::default(), request_tx, request_rx, + UpgradeModeStatus::default(), shutdown_manager.child_shutdown_token(), ); tokio::spawn(async move { peer_controller.run().await }); @@ -542,12 +555,15 @@ pub fn start_controller( } #[cfg(feature = "mock")] +// unwraps are fine in test code +#[allow(clippy::unwrap_used)] pub async fn stop_controller(mut shutdown_manager: nym_task::ShutdownManager) { shutdown_manager.send_cancellation(); shutdown_manager.run_until_shutdown().await; } #[cfg(test)] +#[cfg(feature = "mock")] mod tests { use super::*; diff --git a/common/wireguard/src/peer_handle.rs b/common/wireguard/src/peer_handle.rs index 57c92d5072..b5dd5d83fc 100644 --- a/common/wireguard/src/peer_handle.rs +++ b/common/wireguard/src/peer_handle.rs @@ -6,12 +6,16 @@ use crate::peer_controller::PeerControlRequest; use crate::peer_storage_manager::{CachedPeerManager, PeerInformation}; use defguard_wireguard_rs::{host::Host, key::Key, net::IpAddrMask}; use futures::channel::oneshot; +use nym_credential_verification::OutOfBandwidthResultExt; use nym_credential_verification::bandwidth_storage_manager::BandwidthStorageManager; +use nym_credential_verification::upgrade_mode::UpgradeModeStatus; use nym_task::ShutdownToken; use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK; +use std::fmt::Display; use std::sync::Arc; use tokio::sync::{RwLock, mpsc}; use tokio_stream::{StreamExt, wrappers::IntervalStream}; +use tracing::{debug, error, trace, warn}; #[derive(Clone)] pub(crate) struct SharedBandwidthStorageManager { @@ -43,9 +47,19 @@ pub struct PeerHandle { bandwidth_storage_manager: SharedBandwidthStorageManager, request_tx: mpsc::Sender, timeout_check_interval: IntervalStream, + + /// Flag indicating whether the system is undergoing an upgrade and thus peers shouldn't be getting + /// their bandwidth metered. + upgrade_mode: UpgradeModeStatus, shutdown_token: ShutdownToken, } +impl Display for PeerHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "peer {}", self.public_key) + } +} + impl PeerHandle { pub(crate) fn new( public_key: Key, @@ -53,11 +67,11 @@ impl PeerHandle { cached_peer: CachedPeerManager, bandwidth_storage_manager: SharedBandwidthStorageManager, request_tx: mpsc::Sender, + upgrade_mode: UpgradeModeStatus, shutdown_token: &ShutdownToken, ) -> Self { - let timeout_check_interval = tokio_stream::wrappers::IntervalStream::new( - tokio::time::interval(DEFAULT_PEER_TIMEOUT_CHECK), - ); + let timeout_check_interval = + IntervalStream::new(tokio::time::interval(DEFAULT_PEER_TIMEOUT_CHECK)); let shutdown_token = shutdown_token.clone(); PeerHandle { public_key, @@ -66,10 +80,22 @@ impl PeerHandle { bandwidth_storage_manager, request_tx, timeout_check_interval, + upgrade_mode, shutdown_token, } } + /// Attempt to use the specified amount of bandwidth and update internal cache. + /// Returns the amount of bandwidth remaining + async fn try_use_bandwidth(&self, spent: i64) -> nym_credential_verification::Result { + self.bandwidth_storage_manager + .inner + .write() + .await + .try_use_bandwidth(spent) + .await + } + async fn remove_peer(&self) -> Result { let (response_tx, response_rx) = oneshot::channel(); self.request_tx @@ -87,73 +113,33 @@ impl PeerHandle { Ok(success) } - fn compute_spent_bandwidth( - kernel_peer: PeerInformation, - cached_peer: PeerInformation, - ) -> Option { - let kernel_total = kernel_peer - .rx_bytes - .checked_add(kernel_peer.tx_bytes) - .or_else(|| { - tracing::error!( - "Overflow on kernel adding bytes: {} + {}", - kernel_peer.rx_bytes, - kernel_peer.tx_bytes - ); - None - })?; - let cached_total = cached_peer - .rx_bytes - .checked_add(cached_peer.tx_bytes) - .or_else(|| { - tracing::error!( - "Overflow on cached adding bytes: {} + {}", - cached_peer.rx_bytes, - cached_peer.tx_bytes - ); - None - })?; - - kernel_total.checked_sub(cached_total).or_else(|| { - tracing::error!("Overflow on spent bandwidth subtraction: kernel - cached = {kernel_total} - {cached_total}"); - None - }) - } - async fn active_peer(&mut self, kernel_peer: PeerInformation) -> Result { let Some(cached_peer) = self.cached_peer.get_peer() else { - log::debug!( - "Peer {:?} not in storage anymore, shutting down handle", - self.public_key - ); + debug!("{self} not in storage anymore, shutting down handle"); return Ok(false); }; - let spent_bandwidth = Self::compute_spent_bandwidth(kernel_peer, cached_peer) - .unwrap_or_default() - .try_into() - .inspect_err(|err| tracing::error!("Could not convert from u64 to i64: {err:?}")) - .unwrap_or_default(); - + let spent_bandwidth = kernel_peer.consumed_kernel_bandwidth(&cached_peer); self.cached_peer.update(kernel_peer); - if spent_bandwidth > 0 - && self - .bandwidth_storage_manager - .inner() - .write() - .await + if spent_bandwidth > 0 { + trace!("{self} has used {spent_bandwidth} of bandwidth"); + if self.upgrade_mode.enabled() { + debug!("we're in upgrade mode - {self} is not going to get its bandwidth deducted"); + return Ok(true); + } + + // 'regular' flow + if self .try_use_bandwidth(spent_bandwidth) .await - .is_err() - { - tracing::debug!( - "Peer {} is out of bandwidth, removing it", - self.public_key.to_string() - ); - let success = self.remove_peer().await?; - self.cached_peer.remove_peer(); - return Ok(!success); + .is_out_of_bandwidth() + { + debug!("{self} is out of bandwidth, removing it"); + let success = self.remove_peer().await?; + self.cached_peer.remove_peer(); + return Ok(!success); + } } Ok(true) @@ -169,10 +155,7 @@ impl PeerHandle { .ok_or(Error::MissingClientKernelEntry(self.public_key.to_string()))? .into(); if !self.active_peer(kernel_peer).await? { - log::debug!( - "Peer {:?} is not active anymore, shutting down handle", - self.public_key - ); + debug!("{self} is not active anymore, shutting down handle",); Ok(false) } else { Ok(true) @@ -184,12 +167,12 @@ impl PeerHandle { tokio::select! { biased; _ = self.shutdown_token.cancelled() => { - log::trace!("PeerHandle: Received shutdown"); + trace!("PeerHandle: Received shutdown"); if let Err(e) = self.bandwidth_storage_manager.inner().write().await.sync_storage_bandwidth().await { - log::error!("Storage sync failed - {e}, unaccounted bandwidth might have been consumed"); + error!("Storage sync failed - {e}, unaccounted bandwidth might have been consumed"); } - log::trace!("PeerHandle: Finished shutdown"); + trace!("PeerHandle: Finished shutdown"); break; } _ = self.timeout_check_interval.next() => { @@ -199,11 +182,11 @@ impl PeerHandle { Err(err) => { match self.remove_peer().await { Ok(true) => { - tracing::debug!("Removed peer due to error {err}"); + debug!("Removed peer due to error {err}"); return; } _ => { - tracing::warn!("Could not remove peer yet, we'll try again later. If this message persists, the gateway might need to be restarted"); + warn!("Could not remove peer yet, we'll try again later. If this message persists, the gateway might need to be restarted"); continue; } } diff --git a/common/wireguard/src/peer_storage_manager.rs b/common/wireguard/src/peer_storage_manager.rs index 1675cf6b2f..00c439350d 100644 --- a/common/wireguard/src/peer_storage_manager.rs +++ b/common/wireguard/src/peer_storage_manager.rs @@ -46,7 +46,7 @@ impl CachedPeerManager { pub(crate) fn update(&mut self, kernel_peer: PeerInformation) { if let Some(peer_information) = self.peer_information.as_mut() { - peer_information.update_trx_bytes(kernel_peer); + peer_information.update_tx_rx_bytes(kernel_peer); } } } @@ -67,8 +67,49 @@ impl From<&Peer> for PeerInformation { } impl PeerInformation { - pub(crate) fn update_trx_bytes(&mut self, peer: PeerInformation) { + pub(crate) fn update_tx_rx_bytes(&mut self, peer: PeerInformation) { self.tx_bytes = peer.tx_bytes; self.rx_bytes = peer.rx_bytes; } + + fn rx_tx_total(&self, typ: &'static str) -> Option { + self.rx_bytes.checked_add(self.tx_bytes).or_else(|| { + tracing::error!( + "overflow on {typ} adding bytes: {} + {}", + self.rx_bytes, + self.tx_bytes + ); + None + }) + } + + /// Attempt to determine the amount of consumed bandwidth based on the current peer information + /// and state from the last checkpoint. + pub(crate) fn consumed_bandwidth(kernel: &Self, previous_cached: &Self) -> Option { + let kernel_total = kernel.rx_tx_total("kernel")?; + let cached_total = previous_cached.rx_tx_total("cached")?; + kernel_total.checked_sub(cached_total).or_else(|| { + tracing::error!("Overflow on spent bandwidth subtraction: kernel - cached = {kernel_total} - {cached_total}"); + None + }) + } + + /// Attempt to determine the amount of consumed bandwidth based on the current peer information + /// and state from the last checkpoint. + /// On failures, it will attempt to default to most sensible alternative + /// + /// Note, it is responsibility of the caller to ensure that `self` corresponds to the kernel peer information + pub(crate) fn consumed_kernel_bandwidth(&self, previous_cached: &Self) -> i64 { + let Some(consumed) = Self::consumed_bandwidth(self, previous_cached) else { + // old behaviour of returning the `Default::default()` + return 0; + }; + + // old behaviour would have also returned 0 here, but I'd argue if u64 can't fit in i64, + // it means we're over i64::MAX, thus that's what we should return + consumed + .try_into() + .inspect_err(|err| tracing::error!("Could not convert from u64 to i64: {err:?}")) + .unwrap_or(i64::MAX) + } } diff --git a/envs/canary.env b/envs/canary.env index f494ad3270..fbf97b2643 100644 --- a/envs/canary.env +++ b/envs/canary.env @@ -22,3 +22,5 @@ NYXD=https://rpc.canary-validator.performance.nymte.ch NYM_API=https://canary-api.performance.nymte.ch/api/ NYXD_WS=wss://rpc.canary-validator.performance.nymte.ch/websocket NYM_VPN_API=https://nym-vpn-api-git-deploy-canary-nyx-network-staging.vercel.app/api/ + +UPGRADE_MODE_ATTESTER_ED25519_PUBKEY=U1NXToPYUTsh7pYPLcwXCXwcL6pGoLUou7fyAJrNz8b \ No newline at end of file diff --git a/envs/mainnet.env b/envs/mainnet.env index 444307f836..01e5e9092b 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -25,3 +25,5 @@ NYXD=https://rpc.nymtech.net NYM_API=https://validator.nymtech.net/api/ NYXD_WS=wss://rpc.nymtech.net/websocket NYM_VPN_API=https://nymvpn.com/api/ + +UPGRADE_MODE_ATTESTER_ED25519_PUBKEY=3bgffBYcfFkTTXc2npNNn9MkddFZ3H2LrPjXDmnJzrqd \ No newline at end of file diff --git a/envs/sandbox.env b/envs/sandbox.env index e90257f015..96a46881dd 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -23,3 +23,5 @@ NYXD=https://rpc.sandbox.nymtech.net NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket NYM_API=https://sandbox-nym-api1.nymtech.net/api/ NYM_VPN_API=https://nym-vpn-api-git-deploy-sandbox-nyx-network-staging.vercel.app/api/ + +UPGRADE_MODE_ATTESTER_ED25519_PUBKEY=EGwzKXPrqStv8cHF68VT2LbQuEBGDPzhCAixScvybfem \ No newline at end of file diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 1e4a5be417..cf1b8f286b 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -19,7 +19,6 @@ rust-version = "1.85" path = "src/lib.rs" [dependencies] -anyhow = { workspace = true } bincode = { workspace = true } async-trait = { workspace = true } bip39 = { workspace = true } @@ -30,7 +29,6 @@ futures = { workspace = true } ipnetwork = { workspace = true } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } -sha2 = { workspace = true } thiserror = { workspace = true } time = { workspace = true } tokio = { workspace = true, features = [ @@ -40,16 +38,14 @@ tokio = { workspace = true, features = [ "fs", "time", ] } -tokio-stream = { workspace = true, features = ["fs"] } +tokio-stream = { workspace = true } tokio-tungstenite = { workspace = true } -tokio-util = { workspace = true, features = ["codec"] } tracing = { workspace = true } url = { workspace = true, features = ["serde"] } zeroize = { workspace = true } # internal -nym-api-requests = { path = "../nym-api/nym-api-requests" } nym-credentials = { path = "../common/credentials" } nym-credentials-interface = { path = "../common/credentials-interface" } nym-credential-verification = { path = "../common/credential-verification" } @@ -58,7 +54,6 @@ nym-gateway-storage = { path = "../common/gateway-storage" } nym-gateway-stats-storage = { path = "../common/gateway-stats-storage" } nym-gateway-requests = { path = "../common/gateway-requests" } nym-mixnet-client = { path = "../common/client-libs/mixnet-client" } -nym-mixnode-common = { path = "../common/mixnode-common" } nym-network-defaults = { path = "../common/network-defaults" } nym-network-requester = { path = "../service-providers/network-requester" } nym-sdk = { path = "../sdk/rust/nym-sdk" } @@ -66,10 +61,10 @@ nym-sphinx = { path = "../common/nymsphinx" } nym-statistics-common = { path = "../common/statistics" } nym-task = { path = "../common/task" } nym-topology = { path = "../common/topology" } -nym-types = { path = "../common/types" } nym-validator-client = { path = "../common/client-libs/validator-client" } nym-ip-packet-router = { path = "../service-providers/ip-packet-router" } nym-node-metrics = { path = "../nym-node/nym-node-metrics" } +nym-upgrade-mode-check = { path = "../common/upgrade-mode-check" } nym-wireguard = { path = "../common/wireguard" } nym-wireguard-private-metadata-server = { path = "../common/wireguard-private-metadata/server" } @@ -80,7 +75,6 @@ nym-client-core = { path = "../common/client-core", features = ["cli"] } nym-id = { path = "../common/nym-id" } nym-service-provider-requests-common = { path = "../common/service-provider-requests-common" } - defguard_wireguard_rs = { workspace = true } [dev-dependencies] @@ -88,3 +82,6 @@ nym-gateway-storage = { path = "../common/gateway-storage", features = ["mock"] nym-wireguard = { path = "../common/wireguard", features = ["mock"] } mock_instant = "0.6.0" time = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/gateway/src/config.rs b/gateway/src/config.rs index 363ddbb88e..8df528674b 100644 --- a/gateway/src/config.rs +++ b/gateway/src/config.rs @@ -13,6 +13,8 @@ pub struct Config { pub ip_packet_router: IpPacketRouter, + pub upgrade_mode_watcher: UpgradeModeWatcher, + pub debug: Debug, } @@ -21,12 +23,14 @@ impl Config { gateway: impl Into, network_requester: impl Into, ip_packet_router: impl Into, + upgrade_mode_watcher: impl Into, debug: impl Into, ) -> Self { Config { gateway: gateway.into(), network_requester: network_requester.into(), ip_packet_router: ip_packet_router.into(), + upgrade_mode_watcher: upgrade_mode_watcher.into(), debug: debug.into(), } } @@ -57,6 +61,28 @@ pub struct Gateway { pub nyxd_urls: Vec, } +#[derive(Debug)] +pub struct UpgradeModeWatcher { + /// Specifies whether this gateway watches for upgrade mode changes + /// via the published attestation file. + pub enabled: bool, + + /// Endpoint to query to retrieve current upgrade mode attestation. + /// If not provided, it implicitly disables the watcher and upgrade-mode features + pub attestation_url: Url, + + pub debug: UpgradeModeWatcherDebug, +} + +#[derive(Debug)] +pub struct UpgradeModeWatcherDebug { + /// Default polling interval + pub regular_polling_interval: Duration, + + /// Expedited polling interval for once upgrade mode is detected + pub expedited_poll_interval: Duration, +} + #[derive(Debug, PartialEq)] pub struct NetworkRequester { /// Specifies whether network requester service is enabled in this process. @@ -104,6 +130,9 @@ pub struct Debug { /// Defines the timestamp skew of a signed authentication request before it's deemed too excessive to process. pub max_request_timestamp_skew: Duration, + + /// The minimum duration since the last explicit check for the upgrade mode to allow creation of new requests. + pub upgrade_mode_min_staleness_recheck: Duration, } #[derive(Debug, Clone)] diff --git a/gateway/src/node/client_handling/active_clients.rs b/gateway/src/node/client_handling/active_clients.rs index a215affdf9..8f577ba13f 100644 --- a/gateway/src/node/client_handling/active_clients.rs +++ b/gateway/src/node/client_handling/active_clients.rs @@ -147,7 +147,7 @@ impl ActiveClientsStore { handle: MixMessageSender, is_active_request_sender: IsActiveRequestSender, session_request_timestamp: OffsetDateTime, - ) { + ) -> bool { let entry = ActiveClient::Remote(RemoteClientData { session_request_timestamp, channels: ClientIncomingChannels { @@ -156,11 +156,16 @@ impl ActiveClientsStore { }, }); if self.inner.insert(client, entry).is_some() { - panic!("inserted a duplicate remote client") + // this should be impossible under normal circumstances, + // but in some rare edge cases of clients performing very careful timing attacks, + // this branch could be potentially triggered + return false; } + true } /// Inserts a handle to the embedded client + #[allow(clippy::panic)] pub fn insert_embedded(&self, local_client_handle: LocalEmbeddedClientHandle) { let key = local_client_handle.client_destination(); let entry = ActiveClient::Embedded(Box::new(local_client_handle)); diff --git a/gateway/src/node/client_handling/websocket/common_state.rs b/gateway/src/node/client_handling/websocket/common_state.rs index 7f4d66c828..f3e9f711fa 100644 --- a/gateway/src/node/client_handling/websocket/common_state.rs +++ b/gateway/src/node/client_handling/websocket/common_state.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node::ActiveClientsStore; +use nym_credential_verification::upgrade_mode::UpgradeModeDetails; use nym_credential_verification::{ecash::EcashManager, BandwidthFlushingBehaviourConfig}; use nym_crypto::asymmetric::ed25519; use nym_gateway_storage::GatewayStorage; @@ -11,7 +12,7 @@ use nym_node_metrics::NymNodeMetrics; use std::sync::Arc; use std::time::Duration; -#[derive(Clone)] +#[derive(Clone, Copy)] pub(crate) struct Config { pub(crate) enforce_zk_nym: bool, pub(crate) max_request_timestamp_skew: Duration, @@ -29,6 +30,7 @@ pub(crate) struct CommonHandlerState { pub(crate) metrics_sender: MetricEventsSender, pub(crate) outbound_mix_sender: MixForwardingSender, pub(crate) active_clients_store: ActiveClientsStore, + pub(crate) upgrade_mode: UpgradeModeDetails, } impl CommonHandlerState { diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index 36b8029332..b40642930a 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -14,14 +14,16 @@ use futures::{ future::{FusedFuture, OptionFuture}, FutureExt, StreamExt, }; +use nym_credential_verification::upgrade_mode::UpgradeModeEnableError; use nym_credential_verification::CredentialVerifier; use nym_credential_verification::{ bandwidth_storage_manager::BandwidthStorageManager, ClientBandwidth, }; +use nym_credentials_interface::DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD; use nym_gateway_requests::{ types::{BinaryRequest, ServerResponse}, - ClientControlRequest, ClientRequest, GatewayRequestsError, SensitiveServerResponse, - SimpleGatewayRequestsError, + BandwidthResponse, ClientControlRequest, ClientRequest, GatewayRequestsError, SendResponse, + SensitiveServerResponse, SimpleGatewayRequestsError, }; use nym_gateway_storage::error::GatewayStorageError; use nym_gateway_storage::traits::BandwidthGatewayStorage; @@ -31,6 +33,7 @@ use nym_sphinx::forwarding::packet::MixPacket; use nym_statistics_common::{gateways::GatewaySessionEvent, types::SessionType}; use nym_validator_client::coconut::EcashApiError; use rand::{random, CryptoRng, Rng}; +use std::cmp::max; use std::{process, time::Duration}; use thiserror::Error; use tokio::io::{AsyncRead, AsyncWrite}; @@ -92,8 +95,11 @@ pub enum RequestHandlingError { #[error("failed to recover bandwidth value: {0}")] BandwidthRecoveryFailure(#[from] BandwidthError), - #[error("{0}")] + #[error(transparent)] CredentialVerification(#[from] nym_credential_verification::Error), + + #[error(transparent)] + UpgradeModeEnable(#[from] UpgradeModeEnableError), } impl RequestHandlingError { @@ -161,6 +167,10 @@ impl AuthenticatedHandler { &self.inner } + fn upgrade_mode_enabled(&self) -> bool { + self.inner.upgrade_mode_enabled() + } + /// Upgrades `FreshHandler` into the Authenticated variant implying the client is now authenticated /// and thus allowed to perform more actions with the gateway, such as redeeming bandwidth or /// sending sphinx packets. @@ -271,7 +281,50 @@ impl AuthenticatedHandler { .inspect_err(|verification_failure| debug!("{verification_failure}"))?; trace!("available total bandwidth: {available_total}"); - Ok(ServerResponse::Bandwidth { available_total }) + Ok(ServerResponse::Bandwidth(BandwidthResponse { + available_total, + upgrade_mode: self.upgrade_mode_enabled(), + })) + } + + async fn upgrade_mode_bandwidth(&self) -> i64 { + // if we're undergoing upgrade mode, we don't meter bandwidth, + // we simply return MAX of clients current bandwidth and minimum bandwidth before default + // client would have attempted to send new ticket + // the latter is to support older clients that will ignore `upgrade_mode` field in the response + // as they're not aware of its existence + let available_bandwidth = self.bandwidth_storage_manager.available_bandwidth().await; + max( + DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD + 1, + available_bandwidth, + ) + } + + /// Tries to handle the received JWT token request by checking its correctness and + /// internally enables upgrade mode if it hasn't been set before. + /// Furthermore, clients bandwidth metering is getting disabled. + async fn handle_upgrade_mode_jwt( + &self, + token: String, + ) -> Result { + // if we're already in the upgrade mode, don't bother validating the token + if self.upgrade_mode_enabled() { + return Ok(ServerResponse::Bandwidth(BandwidthResponse { + available_total: self.upgrade_mode_bandwidth().await, + upgrade_mode: true, + })); + } + + self.inner + .shared_state + .upgrade_mode + .try_enable_via_received_jwt(token) + .await?; + + Ok(ServerResponse::Bandwidth(BandwidthResponse { + available_total: self.upgrade_mode_bandwidth().await, + upgrade_mode: true, + })) } /// Tries to handle request to forward sphinx packet into the network. The request can only succeed @@ -289,15 +342,22 @@ impl AuthenticatedHandler { ) -> Result { let required_bandwidth = mix_packet.packet().len() as i64; - let remaining_bandwidth = self - .bandwidth_storage_manager - .try_use_bandwidth(required_bandwidth) - .await?; + let upgrade_mode = self.upgrade_mode_enabled(); + + let remaining_bandwidth = if self.upgrade_mode_enabled() { + self.upgrade_mode_bandwidth().await + } else { + self.bandwidth_storage_manager + .try_use_bandwidth(required_bandwidth) + .await? + }; + self.forward_packet(mix_packet); - Ok(ServerResponse::Send { + Ok(ServerResponse::Send(SendResponse { remaining_bandwidth, - }) + upgrade_mode, + })) } /// Attempts to handle a binary data frame websocket message. @@ -432,6 +492,9 @@ impl AuthenticatedHandler { ClientControlRequest::EcashCredential { enc_credential, iv } => { self.handle_ecash_bandwidth(enc_credential, iv).await } + ClientControlRequest::UpgradeModeJWT { token } => { + self.handle_upgrade_mode_jwt(token).await + } ClientControlRequest::BandwidthCredential { .. } => { Err(RequestHandlingError::IllegalRequest { additional_context: "coconut credential are not longer supported".into(), @@ -446,7 +509,13 @@ impl AuthenticatedHandler { .bandwidth_storage_manager .handle_claim_testnet_bandwidth() .await - .map_err(|e| e.into()), + .map_err(|e| e.into()) + .map(|available_total| { + ServerResponse::Bandwidth(BandwidthResponse { + available_total, + upgrade_mode: self.upgrade_mode_enabled(), + }) + }), ClientControlRequest::SupportedProtocol { .. } => { Ok(self.inner.handle_supported_protocol_request()) } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 54d6e02f6e..0eb18a602a 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -20,11 +20,12 @@ use nym_gateway_requests::authenticate::AuthenticateRequest; use nym_gateway_requests::authentication::encrypted_address::{ EncryptedAddressBytes, EncryptedAddressConversionError, }; +use nym_gateway_requests::registration::handshake::HandshakeResult; use nym_gateway_requests::{ registration::handshake::{error::HandshakeError, gateway_handshake}, types::{ClientControlRequest, ServerResponse}, - AuthenticationFailure, BinaryResponse, SharedGatewayKey, CURRENT_PROTOCOL_VERSION, - INITIAL_PROTOCOL_VERSION, + AuthenticationFailure, BinaryResponse, GatewayProtocolVersion, GatewayProtocolVersionExt, + SharedGatewayKey, CURRENT_PROTOCOL_VERSION, }; use nym_gateway_storage::error::GatewayStorageError; use nym_gateway_storage::traits::BandwidthGatewayStorage; @@ -88,9 +89,6 @@ pub(crate) enum InitialAuthenticationError { #[error("Experienced connection error: {0}")] ConnectionError(Box), - #[error("Attempted to negotiate connection with client using incompatible protocol version. Ours is {current} and the client reports {client:?}")] - IncompatibleProtocol { client: Option, current: u8 }, - #[error("failed to send authentication response: {source}")] ResponseSendFailure { #[source] @@ -130,7 +128,7 @@ pub(crate) struct FreshHandler { pub(crate) shutdown: ShutdownToken, // currently unused (but populated) - pub(crate) negotiated_protocol: Option, + pub(crate) negotiated_protocol: Option, } impl FreshHandler { @@ -138,6 +136,10 @@ impl FreshHandler { &self.shared_state } + pub(crate) fn upgrade_mode_enabled(&self) -> bool { + self.shared_state.upgrade_mode.enabled() + } + // for time being we assume handle is always constructed from raw socket. // if we decide we want to change it, that's not too difficult pub(crate) fn new( @@ -189,7 +191,8 @@ impl FreshHandler { async fn perform_registration_handshake( &mut self, init_msg: Vec, - ) -> Result + requested_protocol: Option, + ) -> Result where S: AsyncRead + AsyncWrite + Unpin + Send, R: CryptoRng + RngCore + Send, @@ -202,15 +205,17 @@ impl FreshHandler { ws_stream, self.shared_state.local_identity.as_ref(), init_msg, + requested_protocol, self.shutdown.clone(), ) .await } - _ => unreachable!(), + _ => Err(HandshakeError::ConnectionInInvalidState), } } /// Attempts to read websocket message from the associated socket. + #[allow(clippy::panic)] pub(crate) async fn read_websocket_message(&mut self) -> Option> where S: AsyncRead + AsyncWrite + Unpin, @@ -226,6 +231,7 @@ impl FreshHandler { /// # Arguments /// /// * `msg`: WebSocket message to write back to the client. + #[allow(clippy::panic)] pub(crate) async fn send_websocket_message( &mut self, msg: impl Into, @@ -269,6 +275,7 @@ impl FreshHandler { /// /// * `shared_keys`: keys derived between the client and gateway. /// * `packets`: unwrapped packets that are to be pushed back to the client. + #[allow(clippy::panic)] pub(crate) async fn push_packets_to_client( &mut self, shared_keys: &SharedGatewayKey, @@ -411,59 +418,6 @@ impl FreshHandler { } } - fn negotiate_client_protocol( - &self, - client_protocol: Option, - ) -> Result { - debug!("client protocol: {client_protocol:?}, ours: {CURRENT_PROTOCOL_VERSION}"); - let Some(client_protocol_version) = client_protocol else { - warn!("the client we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0"); - // note: in +1.2.0 we will have to return a hard error here - return Ok(INITIAL_PROTOCOL_VERSION); - }; - - // ##### - // On backwards compat: - // Currently it is the case that gateways will understand all previous protocol versions - // and will downgrade accordingly, but this will now always be the case. - // For example, once we remove downgrade on legacy auth, anything below version 4 will be rejected - // ##### - - // a v2 gateway will understand v1 requests, but v1 client will not understand v2 responses - if client_protocol_version == 1 { - return Ok(1); - } - - // a v3 gateway will understand v2 requests (legacy keys) - if client_protocol_version == 2 { - return Ok(2); - } - - // a v4 gateway will understand v3 requests (aes256gcm-siv) - if client_protocol_version == 3 { - return Ok(3); - } - - // a v5 gateway will understand v4 requests (key-rotation) - if client_protocol_version == 4 { - return Ok(4); - } - - // we can't handle clients with higher protocol than ours - // (perhaps we could try to negotiate downgrade on our end? sounds like a nice future improvement) - if client_protocol_version <= CURRENT_PROTOCOL_VERSION { - debug!("the client is using exactly the same (or older) protocol version as we are. We're good to continue!"); - Ok(CURRENT_PROTOCOL_VERSION) - } else { - let err = InitialAuthenticationError::IncompatibleProtocol { - client: client_protocol, - current: CURRENT_PROTOCOL_VERSION, - }; - error!("{err}"); - Err(err) - } - } - async fn handle_duplicate_client( &mut self, address: DestinationAddressBytes, @@ -551,6 +505,29 @@ impl FreshHandler { Ok(available_bandwidth) } + fn negotiate_proposed_protocol( + &self, + client_protocol_version: Option, + ) -> Option { + if client_protocol_version.is_future_version() { + // this should never happen in a non-malicious client as it should use at most whatever version this gateway has announced + warn!("client has announced protocol version greater than one known by this gateway (v{client_protocol_version:?} vs v{}). attempting to downgrade.", GatewayProtocolVersion::CURRENT); + // we just reply with our current version, and it's up to the client to accept it or terminate the connection + Some(GatewayProtocolVersion::CURRENT) + } else { + // ##### + // On backwards compat: + // Currently it is the case that gateways will understand all previous protocol versions + // and will downgrade accordingly, but this will not always be the case. + // For example, once we remove downgrade on legacy auth, anything below version 4 will be rejected + // ##### + debug!( + "using the protocol version proposed by the client: v{client_protocol_version:?}" + ); + client_protocol_version + } + } + /// Tries to handle the received authentication request by checking correctness of the received data. /// /// # Arguments @@ -565,7 +542,7 @@ impl FreshHandler { )] async fn handle_legacy_authenticate( &mut self, - client_protocol_version: Option, + client_protocol_version: Option, address: String, enc_address: String, raw_nonce: String, @@ -575,9 +552,9 @@ impl FreshHandler { { debug!("handling client authentication (v1)"); - let negotiated_protocol = self.negotiate_client_protocol(client_protocol_version)?; + let negotiated_protocol = self.negotiate_proposed_protocol(client_protocol_version); // populate the negotiated protocol for future uses - self.negotiated_protocol = Some(negotiated_protocol); + self.negotiated_protocol = negotiated_protocol; let address = DestinationAddressBytes::try_from_base58_string(address) .map_err(|err| InitialAuthenticationError::MalformedClientAddress(err.to_string()))?; @@ -592,7 +569,7 @@ impl FreshHandler { .await? else { // it feels weird to be returning an 'Ok' here, but I didn't want to change the existing behaviour - return Ok(InitialAuthResult::new_failed(Some(negotiated_protocol))); + return Ok(InitialAuthResult::new_legacy_failed(negotiated_protocol)); }; // in v1 we don't have explicit data so we have to use current timestamp @@ -634,9 +611,10 @@ impl FreshHandler { session_request_start, )), ServerResponse::Authenticate { - protocol_version: Some(negotiated_protocol), + protocol_version: negotiated_protocol, status: true, bandwidth_remaining, + upgrade_mode: self.upgrade_mode_enabled(), }, )) } @@ -651,9 +629,9 @@ impl FreshHandler { debug!("handling client authentication (v2)"); let negotiated_protocol = - self.negotiate_client_protocol(Some(request.content.protocol_version))?; + self.negotiate_proposed_protocol(Some(request.content.protocol_version)); // populate the negotiated protocol for future uses - self.negotiated_protocol = Some(negotiated_protocol); + self.negotiated_protocol = negotiated_protocol; let address = request.content.client_identity.derive_destination_address(); @@ -720,9 +698,10 @@ impl FreshHandler { session_request_start, )), ServerResponse::Authenticate { - protocol_version: Some(negotiated_protocol), + protocol_version: negotiated_protocol, status: true, bandwidth_remaining, + upgrade_mode: self.upgrade_mode_enabled(), }, )) } @@ -782,17 +761,13 @@ impl FreshHandler { /// * `init_data`: init payload of the registration handshake. async fn handle_register( &mut self, - client_protocol_version: Option, + client_protocol_version: Option, init_data: Vec, ) -> Result where S: AsyncRead + AsyncWrite + Unpin + Send, R: CryptoRng + RngCore + Send, { - let negotiated_protocol = self.negotiate_client_protocol(client_protocol_version)?; - // populate the negotiated protocol for future uses - self.negotiated_protocol = Some(negotiated_protocol); - let remote_identity = Self::extract_remote_identity_from_register_init(&init_data)?; let remote_address = remote_identity.derive_destination_address(); @@ -806,11 +781,20 @@ impl FreshHandler { return Err(InitialAuthenticationError::DuplicateConnection); } - let shared_keys = self.perform_registration_handshake(init_data).await?; + let handshake_result = self + .perform_registration_handshake(init_data, client_protocol_version) + .await?; + let shared_keys = handshake_result.derived_key; + + // populate the negotiated protocol for future uses + self.negotiated_protocol = Some(handshake_result.negotiated_protocol); + let client_id = self.register_client(remote_address, &shared_keys).await?; debug!(client_id = %client_id, "managed to finalize client registration"); + let upgrade_mode = self.upgrade_mode_enabled(); + let client_details = ClientDetails::new( client_id, remote_address, @@ -821,8 +805,9 @@ impl FreshHandler { Ok(InitialAuthResult::new( Some(client_details), ServerResponse::Register { - protocol_version: Some(negotiated_protocol), + protocol_version: self.negotiated_protocol, status: true, + upgrade_mode, }, )) } @@ -946,12 +931,15 @@ impl FreshHandler { let (mix_sender, mix_receiver) = mpsc::unbounded(); // Channel for handlers to ask other handlers if they are still active. let (is_active_request_sender, is_active_request_receiver) = mpsc::unbounded(); - self.shared_state.active_clients_store.insert_remote( + if !self.shared_state.active_clients_store.insert_remote( registration_details.address, mix_sender, is_active_request_sender, registration_details.session_request_timestamp, - ); + ) { + error!("failed to insert remote client handle as it already existed!"); + return None; + } return AuthenticatedHandler::upgrade( self, diff --git a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs index 2fd97600a7..b8ecf12ec8 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs @@ -79,13 +79,16 @@ impl InitialAuthResult { } } - fn new_failed(protocol_version: Option) -> Self { + fn new_legacy_failed(protocol_version: Option) -> Self { InitialAuthResult { client_details: None, server_response: ServerResponse::Authenticate { protocol_version, status: false, bandwidth_remaining: 0, + // given this response is given only to legacy clients, + // we use the default value as clients wouldn't deserialise it anyway + upgrade_mode: false, }, } } diff --git a/gateway/src/node/internal_service_providers/authenticator/error.rs b/gateway/src/node/internal_service_providers/authenticator/error.rs index ac9b4fd261..5bdde15919 100644 --- a/gateway/src/node/internal_service_providers/authenticator/error.rs +++ b/gateway/src/node/internal_service_providers/authenticator/error.rs @@ -3,7 +3,9 @@ use ipnetwork::IpNetworkError; use nym_client_core::error::ClientCoreError; +use nym_credential_verification::upgrade_mode::UpgradeModeEnableError; use nym_id::NymIdError; +use nym_service_provider_requests_common::ProtocolError; #[derive(thiserror::Error, Debug)] pub enum AuthenticatorError { @@ -17,9 +19,6 @@ pub enum AuthenticatorError { #[error("{0}")] CredentialVerificationError(#[from] nym_credential_verification::Error), - #[error("invalid credential type")] - InvalidCredentialType, - #[error("the entity wrapping the network requester has disconnected")] DisconnectedParent, @@ -50,6 +49,12 @@ pub enum AuthenticatorError { #[error("internal error: {0}")] InternalError(String), + #[error(transparent)] + InvalidPacketHeader { + #[from] + source: ProtocolError, + }, + #[error("received packet has an invalid type: {0}")] InvalidPacketType(u8), @@ -100,4 +105,15 @@ pub enum AuthenticatorError { #[error("no credential received")] NoCredentialReceived, + + #[error(transparent)] + UpgradeModeEnable(#[from] UpgradeModeEnableError), +} + +impl AuthenticatorError { + pub fn response_serialisation(source: impl Into>) -> Self { + AuthenticatorError::FailedToSerializeResponsePacket { + source: source.into(), + } + } } diff --git a/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs b/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs index af476bfba7..c05d9d8cd6 100644 --- a/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs @@ -1,12 +1,6 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::{ - net::IpAddr, - sync::Arc, - time::{Duration, SystemTime}, -}; - use crate::node::internal_service_providers::authenticator::{ config::Config, error::AuthenticatorError, peer_manager::PeerManager, seen_credential_cache::SeenCredentialCache, @@ -14,47 +8,58 @@ use crate::node::internal_service_providers::authenticator::{ use defguard_wireguard_rs::net::IpAddrMask; use defguard_wireguard_rs::{host::Peer, key::Key}; use futures::StreamExt; -use nym_authenticator_requests::{ - latest::registration::RegistrationData, v4::registration::IpPair, -}; +use nym_authenticator_requests::models::BandwidthClaim; +use nym_authenticator_requests::traits::UpgradeModeMessage; +use nym_authenticator_requests::{latest, v4::registration::IpPair}; use nym_authenticator_requests::{ latest::registration::{GatewayClient, PendingRegistrations, PrivateIPs}, request::AuthenticatorRequest, traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage}, - v1, v2, v3, v4, v5, AuthenticatorVersion, CURRENT_VERSION, + v1, v2, v3, v4, v5, v6, AuthenticatorVersion, CURRENT_VERSION, }; use nym_credential_verification::ecash::traits::EcashManager; +use nym_credential_verification::upgrade_mode::UpgradeModeDetails; use nym_credential_verification::{ bandwidth_storage_manager::BandwidthStorageManager, BandwidthFlushingBehaviourConfig, ClientBandwidth, CredentialVerifier, }; -use nym_credentials_interface::{CredentialSpendingData, TicketType}; +use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData}; use nym_crypto::asymmetric::x25519::KeyPair; use nym_gateway_requests::models::CredentialSpendingRequest; use nym_gateway_storage::models::PersistedBandwidth; use nym_sdk::mixnet::{ AnonymousSenderTag, InputMessage, MixnetMessageSender, Recipient, TransmissionLane, }; -use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; +use nym_service_provider_requests_common::{Protocol, ServiceProviderTypeExt}; use nym_sphinx::receiver::ReconstructedMessage; use nym_task::ShutdownToken; use nym_wireguard::WireguardGatewayData; use nym_wireguard_types::PeerPublicKey; use rand::{prelude::IteratorRandom, thread_rng}; +use std::cmp::max; +use std::{ + net::IpAddr, + sync::Arc, + time::{Duration, SystemTime}, +}; use tokio::sync::RwLock; use tokio_stream::wrappers::IntervalStream; type AuthenticatorHandleResult = Result<(Vec, Option), AuthenticatorError>; const DEFAULT_REGISTRATION_TIMEOUT_CHECK: Duration = Duration::from_secs(60); // 1 minute -pub(crate) struct RegistredAndFree { +// we need to be above MINIMUM_REMAINING_BANDWIDTH (500MB) plus we also have to trick the client +// its depletion is low enough to not require sending new tickets +const DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD: i64 = 1024 * 1024 * 1024; + +pub(crate) struct RegisteredAndFree { registration_in_progres: PendingRegistrations, free_private_network_ips: PrivateIPs, } -impl RegistredAndFree { +impl RegisteredAndFree { pub(crate) fn new(free_private_network_ips: PrivateIPs) -> Self { - RegistredAndFree { + RegisteredAndFree { registration_in_progres: Default::default(), free_private_network_ips, } @@ -69,10 +74,12 @@ pub(crate) struct MixnetListener { pub(crate) mixnet_client: nym_sdk::mixnet::MixnetClient, // Registrations awaiting confirmation - pub(crate) registred_and_free: RwLock, + pub(crate) registered_and_free: RwLock, pub(crate) peer_manager: PeerManager, + pub(crate) upgrade_mode: UpgradeModeDetails, + pub(crate) ecash_verifier: Arc, pub(crate) timeout_check_interval: IntervalStream, @@ -86,6 +93,7 @@ impl MixnetListener { free_private_network_ips: PrivateIPs, wireguard_gateway_data: WireguardGatewayData, mixnet_client: nym_sdk::mixnet::MixnetClient, + upgrade_mode: UpgradeModeDetails, ecash_verifier: Arc, ) -> Self { let timeout_check_interval = @@ -93,27 +101,45 @@ impl MixnetListener { MixnetListener { config, mixnet_client, - registred_and_free: RwLock::new(RegistredAndFree::new(free_private_network_ips)), + registered_and_free: RwLock::new(RegisteredAndFree::new(free_private_network_ips)), peer_manager: PeerManager::new(wireguard_gateway_data), + upgrade_mode, ecash_verifier, timeout_check_interval, seen_credential_cache: SeenCredentialCache::new(), } } + fn upgrade_mode_enabled(&self) -> bool { + self.upgrade_mode.enabled() + } + fn keypair(&self) -> &Arc { self.peer_manager.wireguard_gateway_data.keypair() } + async fn upgrade_mode_bandwidth(&self, peer: PeerPublicKey) -> Result { + // if we're undergoing upgrade mode, we don't meter bandwidth, + // we simply return MAX of clients current bandwidth and minimum bandwidth before default + // client would have attempted to send new ticket (hopefully) + // the latter is to support older clients that will ignore `upgrade_mode` field in the response + // as they're not aware of its existence + let available_bandwidth = self.peer_manager.query_bandwidth(peer).await?; + Ok(max( + DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD, + available_bandwidth, + )) + } + async fn remove_stale_registrations(&self) -> Result<(), AuthenticatorError> { - let mut registred_and_free = self.registred_and_free.write().await; - let registred_values: Vec<_> = registred_and_free + let mut registered_and_free = self.registered_and_free.write().await; + let registered_values: Vec<_> = registered_and_free .registration_in_progres .values() .cloned() .collect(); - for reg in registred_values { - let ip = registred_and_free + for reg in registered_values { + let ip = registered_and_free .free_private_network_ips .get_mut(®.gateway_data.private_ips) .ok_or(AuthenticatorError::InternalDataCorruption(format!( @@ -122,7 +148,7 @@ impl MixnetListener { )))?; let Some(timestamp) = ip else { - registred_and_free + registered_and_free .registration_in_progres .remove(®.gateway_data.pub_key()); tracing::debug!( @@ -138,7 +164,7 @@ impl MixnetListener { })?; if duration > DEFAULT_REGISTRATION_TIMEOUT_CHECK { *ip = None; - registred_and_free + registered_and_free .registration_in_progres .remove(®.gateway_data.pub_key()); tracing::debug!( @@ -159,8 +185,8 @@ impl MixnetListener { ) -> AuthenticatorHandleResult { let remote_public = init_message.pub_key(); let nonce: u64 = fastrand::u64(..); - let mut registred_and_free = self.registred_and_free.write().await; - if let Some(registration_data) = registred_and_free + let mut registered_and_free = self.registered_and_free.write().await; + if let Some(registration_data) = registered_and_free .registration_in_progres .get(&remote_public) { @@ -181,9 +207,7 @@ impl MixnetListener { reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V2 => { v2::response::AuthenticatorResponse::new_pending_registration_success( @@ -201,9 +225,7 @@ impl MixnetListener { reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V3 => { v3::response::AuthenticatorResponse::new_pending_registration_success( @@ -221,38 +243,49 @@ impl MixnetListener { reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V4 => { v4::response::AuthenticatorResponse::new_pending_registration_success( v4::registration::RegistrationData { nonce: registration_data.nonce, - gateway_data: registration_data.gateway_data.clone().into(), + // convert current to v5 and then v5 to v4 (current as of 28.08.25) + gateway_data: v5::registration::GatewayClient::from( + registration_data.gateway_data.clone(), + ) + .into(), wg_port: registration_data.wg_port, }, request_id, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V5 => { v5::response::AuthenticatorResponse::new_pending_registration_success( v5::registration::RegistrationData { nonce: registration_data.nonce, - gateway_data: registration_data.gateway_data.clone(), + gateway_data: registration_data.gateway_data.clone().into(), wg_port: registration_data.wg_port, }, request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? + } + AuthenticatorVersion::V6 => { + v6::response::AuthenticatorResponse::new_pending_registration_success( + v6::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data.clone(), + wg_port: registration_data.wg_port, + }, + request_id, + self.upgrade_mode_enabled(), + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), }; @@ -265,7 +298,7 @@ impl MixnetListener { .allowed_ips .iter() .find_map(|ip_mask| match ip_mask.ip { - std::net::IpAddr::V4(ipv4_addr) => Some(ipv4_addr), + IpAddr::V4(ipv4_addr) => Some(ipv4_addr), _ => None, }) .ok_or(AuthenticatorError::InternalError( @@ -275,14 +308,14 @@ impl MixnetListener { .allowed_ips .iter() .find_map(|ip_mask| match ip_mask.ip { - std::net::IpAddr::V6(ipv6_addr) => Some(ipv6_addr), + IpAddr::V6(ipv6_addr) => Some(ipv6_addr), _ => None, }) .unwrap_or(IpPair::from(IpAddr::from(allowed_ipv4)).ipv6); let bytes = match AuthenticatorVersion::from(protocol) { AuthenticatorVersion::V1 => v1::response::AuthenticatorResponse::new_registered( - v1::registration::RegistredData { - pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + v1::registration::RegisteredData { + pub_key: self.keypair().public_key().into(), private_ip: allowed_ipv4.into(), wg_port: self.config.authenticator.tunnel_announced_port, }, @@ -290,12 +323,10 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::new_registered( - v2::registration::RegistredData { - pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + v2::registration::RegisteredData { + pub_key: self.keypair().public_key().into(), private_ip: allowed_ipv4.into(), wg_port: self.config.authenticator.tunnel_announced_port, }, @@ -303,12 +334,10 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::new_registered( - v3::registration::RegistredData { - pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + v3::registration::RegisteredData { + pub_key: self.keypair().public_key().into(), private_ip: allowed_ipv4.into(), wg_port: self.config.authenticator.tunnel_announced_port, }, @@ -316,12 +345,10 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_registered( - v4::registration::RegistredData { - pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + v4::registration::RegisteredData { + pub_key: self.keypair().public_key().into(), private_ips: (allowed_ipv4, allowed_ipv6).into(), wg_port: self.config.authenticator.tunnel_announced_port, }, @@ -329,27 +356,34 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_registered( - v5::registration::RegistredData { - pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + v5::registration::RegisteredData { + pub_key: self.keypair().public_key().into(), private_ips: (allowed_ipv4, allowed_ipv6).into(), wg_port: self.config.authenticator.tunnel_announced_port, }, request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })?, + .map_err(AuthenticatorError::response_serialisation)?, + AuthenticatorVersion::V6 => v6::response::AuthenticatorResponse::new_registered( + v6::registration::RegisteredData { + pub_key: self.keypair().public_key().into(), + private_ips: (allowed_ipv4, allowed_ipv6).into(), + wg_port: self.config.authenticator.tunnel_announced_port, + }, + request_id, + self.upgrade_mode_enabled(), + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), }; return Ok((bytes, reply_to)); } - let private_ip_ref = registred_and_free + let private_ip_ref = registered_and_free .free_private_network_ips .iter_mut() .filter(|r| r.1.is_none()) @@ -364,12 +398,12 @@ impl MixnetListener { *private_ip_ref.0, nonce, ); - let registration_data = RegistrationData { + let registration_data = latest::registration::RegistrationData { nonce, gateway_data: gateway_data.clone(), wg_port: self.config.authenticator.tunnel_announced_port, }; - registred_and_free + registered_and_free .registration_in_progres .insert(remote_public, registration_data.clone()); let bytes = match AuthenticatorVersion::from(protocol) { @@ -389,9 +423,7 @@ impl MixnetListener { reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V2 => { v2::response::AuthenticatorResponse::new_pending_registration_success( @@ -409,9 +441,7 @@ impl MixnetListener { reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V3 => { v3::response::AuthenticatorResponse::new_pending_registration_success( @@ -429,38 +459,49 @@ impl MixnetListener { reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V4 => { v4::response::AuthenticatorResponse::new_pending_registration_success( v4::registration::RegistrationData { nonce: registration_data.nonce, - gateway_data: registration_data.gateway_data.into(), + // convert current to v5 and then v5 to v4 (current as of 28.08.25) + gateway_data: v5::registration::GatewayClient::from( + registration_data.gateway_data.clone(), + ) + .into(), wg_port: registration_data.wg_port, }, request_id, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V5 => { v5::response::AuthenticatorResponse::new_pending_registration_success( v5::registration::RegistrationData { nonce: registration_data.nonce, - gateway_data: registration_data.gateway_data, + gateway_data: registration_data.gateway_data.into(), wg_port: registration_data.wg_port, }, request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? + } + AuthenticatorVersion::V6 => { + v6::response::AuthenticatorResponse::new_pending_registration_success( + v6::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data, + wg_port: registration_data.wg_port, + }, + request_id, + self.upgrade_mode_enabled(), + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), }; @@ -468,6 +509,29 @@ impl MixnetListener { Ok((bytes, reply_to)) } + async fn handle_final_credential_claim( + &self, + claim: BandwidthClaim, + client_id: i64, + ) -> Result<(), AuthenticatorError> { + match claim.credential { + BandwidthCredential::ZkNym(zk_nym) => { + // if we got zk-nym, we just try to verify it + credential_verification(self.ecash_verifier.clone(), *zk_nym, client_id).await?; + Ok(()) + } + BandwidthCredential::UpgradeModeJWT { token } => { + // if we're already in the upgrade mode, don't bother validating the token + if self.upgrade_mode_enabled() { + return Ok(()); + } + + self.upgrade_mode.try_enable_via_received_jwt(token).await?; + Ok(()) + } + } + } + async fn on_final_request( &mut self, final_message: Box, @@ -475,8 +539,8 @@ impl MixnetListener { request_id: u64, reply_to: Option, ) -> AuthenticatorHandleResult { - let mut registred_and_free = self.registred_and_free.write().await; - let registration_data = registred_and_free + let mut registered_and_free = self.registered_and_free.write().await; + let registration_data = registered_and_free .registration_in_progres .get(&final_message.gateway_client_pub_key()) .ok_or(AuthenticatorError::RegistrationNotInProgress)? @@ -497,28 +561,31 @@ impl MixnetListener { 128, )); + // ideally credential wouldn't have been required in upgrade mode, + // however, we need some basic information to insert valid wg peer let Some(credential) = final_message.credential() else { return Err(AuthenticatorError::NoCredentialReceived); }; + + let typ = credential.kind; + let client_id = self .ecash_verifier .storage() - .insert_wireguard_peer( - &peer, - TicketType::try_from_encoded(credential.payment.t_type) - .map_err(|_| AuthenticatorError::InvalidCredentialType)? - .into(), - ) + .insert_wireguard_peer(&peer, typ.into()) .await?; - if let Err(e) = - credential_verification(self.ecash_verifier.clone(), credential, client_id).await + + if let Err(err) = self + .handle_final_credential_claim(credential, client_id) + .await { self.ecash_verifier .storage() .remove_wireguard_peer(&peer.public_key.to_string()) .await?; - return Err(e); + return Err(err); } + let public_key = peer.public_key.to_string(); if let Err(e) = self.peer_manager.add_peer(peer).await { self.ecash_verifier @@ -528,13 +595,13 @@ impl MixnetListener { return Err(e); } - registred_and_free + registered_and_free .registration_in_progres .remove(&final_message.gateway_client_pub_key()); let bytes = match AuthenticatorVersion::from(protocol) { AuthenticatorVersion::V1 => v1::response::AuthenticatorResponse::new_registered( - v1::registration::RegistredData { + v1::registration::RegisteredData { pub_key: registration_data.gateway_data.pub_key, private_ip: registration_data.gateway_data.private_ips.ipv4.into(), wg_port: registration_data.wg_port, @@ -543,9 +610,9 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::new_registered( - v2::registration::RegistredData { + v2::registration::RegisteredData { pub_key: registration_data.gateway_data.pub_key, private_ip: registration_data.gateway_data.private_ips.ipv4.into(), wg_port: registration_data.wg_port, @@ -554,9 +621,9 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::new_registered( - v3::registration::RegistredData { + v3::registration::RegisteredData { pub_key: registration_data.gateway_data.pub_key, private_ip: registration_data.gateway_data.private_ips.ipv4.into(), wg_port: registration_data.wg_port, @@ -565,28 +632,43 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_registered( - v4::registration::RegistredData { + v4::registration::RegisteredData { + pub_key: registration_data.gateway_data.pub_key, + // convert current to v5 and then v5 to v4 (current as of 28.08.25) + private_ips: v5::registration::IpPair::from( + registration_data.gateway_data.private_ips, + ) + .into(), + wg_port: registration_data.wg_port, + }, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, + request_id, + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)?, + AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_registered( + v5::registration::RegisteredData { pub_key: registration_data.gateway_data.pub_key, private_ips: registration_data.gateway_data.private_ips.into(), wg_port: registration_data.wg_port, }, - reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, - AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_registered( - v5::registration::RegistredData { + .map_err(AuthenticatorError::response_serialisation)?, + AuthenticatorVersion::V6 => v6::response::AuthenticatorResponse::new_registered( + v6::registration::RegisteredData { pub_key: registration_data.gateway_data.pub_key, private_ips: registration_data.gateway_data.private_ips, wg_port: registration_data.wg_port, }, request_id, + self.upgrade_mode_enabled(), ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), }; Ok((bytes, reply_to)) @@ -599,7 +681,12 @@ impl MixnetListener { request_id: u64, reply_to: Option, ) -> AuthenticatorHandleResult { - let available_bandwidth = self.peer_manager.query_bandwidth(msg.pub_key()).await?; + let available_bandwidth = if self.upgrade_mode_enabled() { + self.upgrade_mode_bandwidth(msg.pub_key()).await? + } else { + self.peer_manager.query_bandwidth(msg.pub_key()).await? + }; + let bytes = match AuthenticatorVersion::from(protocol) { AuthenticatorVersion::V1 => { v1::response::AuthenticatorResponse::new_remaining_bandwidth( @@ -611,9 +698,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V2 => { v2::response::AuthenticatorResponse::new_remaining_bandwidth( @@ -624,9 +709,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V3 => { v3::response::AuthenticatorResponse::new_remaining_bandwidth( @@ -637,9 +720,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V4 => { v4::response::AuthenticatorResponse::new_remaining_bandwidth( @@ -650,9 +731,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V5 => { v5::response::AuthenticatorResponse::new_remaining_bandwidth( @@ -662,15 +741,25 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? + } + AuthenticatorVersion::V6 => { + v6::response::AuthenticatorResponse::new_remaining_bandwidth( + Some(v6::registration::RemainingBandwidthData { + available_bandwidth, + }), + request_id, + self.upgrade_mode_enabled(), + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), }; Ok((bytes, reply_to)) } + // if we received a topup request, don't do anything with the upgrade mode async fn on_topup_bandwidth_request( &mut self, msg: Box, @@ -693,6 +782,15 @@ impl MixnetListener { }; let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::V6 => v6::response::AuthenticatorResponse::new_topup_bandwidth( + v6::registration::RemainingBandwidthData { + available_bandwidth, + }, + request_id, + self.upgrade_mode_enabled(), + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_topup_bandwidth( v5::registration::RemainingBandwidthData { available_bandwidth, @@ -700,7 +798,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_topup_bandwidth( v4::registration::RemainingBandwidthData { available_bandwidth, @@ -709,7 +807,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::new_topup_bandwidth( v3::registration::RemainingBandwidthData { available_bandwidth, @@ -718,7 +816,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V1 | AuthenticatorVersion::V2 | AuthenticatorVersion::UNKNOWN => { return Err(AuthenticatorError::UnknownVersion) } @@ -727,6 +825,46 @@ impl MixnetListener { Ok((bytes, reply_to)) } + async fn on_upgrade_mode_check( + &mut self, + msg: Box, + protocol: Protocol, + request_id: u64, + ) -> AuthenticatorHandleResult { + // if upgrade mode is already enabled, we don't need to perform any additional checks + if !self.upgrade_mode_enabled() { + // currently upgrade mode JWT is the only type of emergency credentials supported + if let Some(upgrade_mode_jwt) = msg.upgrade_mode_global_attestation_jwt() { + self.upgrade_mode + .try_enable_via_received_jwt(upgrade_mode_jwt) + .await?; + } + } + + let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::UNKNOWN + | AuthenticatorVersion::V1 + | AuthenticatorVersion::V2 + | AuthenticatorVersion::V3 + | AuthenticatorVersion::V4 + | AuthenticatorVersion::V5 => { + // pre v6 this message hasn't existed + return Err(AuthenticatorError::UnknownVersion); + } + AuthenticatorVersion::V6 => { + v6::response::AuthenticatorResponse::new_upgrade_mode_check( + request_id, + self.upgrade_mode_enabled(), + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)? + } + }; + + // no need to support reply_to, as this is never set in v6 and older versions do not include this message + Ok((bytes, None)) + } + fn received_retry(&self, msg: &(dyn TopUpMessage + Send + Sync + 'static)) -> bool { if let Some(peer_pub_key) = self .seen_credential_cache @@ -787,6 +925,11 @@ impl MixnetListener { self.on_topup_bandwidth_request(msg, protocol, request_id, reply_to) .await } + AuthenticatorRequest::CheckUpgradeMode { + msg, + protocol, + request_id, + } => self.on_upgrade_mode_check(msg, protocol, request_id).await, } } @@ -893,64 +1036,68 @@ async fn credential_verification( fn deserialize_request( reconstructed: &ReconstructedMessage, ) -> Result { - let request_version = *reconstructed + let header = reconstructed .message .first_chunk::<2>() .ok_or(AuthenticatorError::ShortPacket)?; - // Check version of the request and convert to the latest version if necessary - match request_version { - [1, _] => v1::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + let version = header[0]; + + // special case for v1 request where service provider information hasn't been exposed in the header + if version == v1::VERSION { + return v1::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) - .map(Into::into), - [2, request_type] => { - if request_type == ServiceProviderType::Authenticator as u8 { - v2::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) - .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { - source: err, - }) - .map(Into::::into) - .map(Into::into) - } else { - Err(AuthenticatorError::InvalidPacketType(request_type)) - } + .map(Into::into); + } + + let protocol = Protocol::try_from(header)?; + + if !protocol.service_provider_type.is_authenticator() { + return Err(AuthenticatorError::InvalidPacketType( + protocol.service_provider_type as u8, + )); + } + + let version = AuthenticatorVersion::from(protocol.version); + + // Check version of the request and convert to the latest version if necessary + match version { + AuthenticatorVersion::V1 => { + // this branch should be unreachable as v1 has already been handled independently + Err(AuthenticatorError::UnknownVersion) } - [3, request_type] => { - if request_type == ServiceProviderType::Authenticator as u8 { - v3::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) - .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { - source: err, - }) - .map(Into::into) - } else { - Err(AuthenticatorError::InvalidPacketType(request_type)) - } + AuthenticatorVersion::V2 => { + v2::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) + .map(Into::::into) + .map(Into::into) } - [4, request_type] => { - if request_type == ServiceProviderType::Authenticator as u8 { - v4::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) - .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { - source: err, - }) - .map(Into::into) - } else { - Err(AuthenticatorError::InvalidPacketType(request_type)) - } + AuthenticatorVersion::V3 => { + v3::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) + .map(Into::into) } - [5, request_type] => { - if request_type == ServiceProviderType::Authenticator as u8 { - v5::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) - .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { - source: err, - }) - .map(Into::into) - } else { - Err(AuthenticatorError::InvalidPacketType(request_type)) - } + AuthenticatorVersion::V4 => { + v4::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) + .map(Into::into) } - [version, _] => { - tracing::info!("Received packet with invalid version: v{version}"); - Err(AuthenticatorError::InvalidPacketVersion(version)) + AuthenticatorVersion::V5 => { + v5::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) + .map(Into::into) + } + AuthenticatorVersion::V6 => { + v6::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) + .map(Into::into) + } + AuthenticatorVersion::UNKNOWN => { + tracing::info!( + "Received packet with invalid version: v{}", + protocol.version + ); + Err(AuthenticatorError::InvalidPacketVersion(protocol.version)) } } } diff --git a/gateway/src/node/internal_service_providers/authenticator/mod.rs b/gateway/src/node/internal_service_providers/authenticator/mod.rs index 358bc5cb30..f63a86fcc2 100644 --- a/gateway/src/node/internal_service_providers/authenticator/mod.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mod.rs @@ -11,6 +11,9 @@ use nym_task::ShutdownTracker; use nym_wireguard::WireguardGatewayData; use std::{net::IpAddr, path::Path, sync::Arc, time::SystemTime}; +pub use config::Config; +use nym_credential_verification::upgrade_mode::UpgradeModeDetails; + pub mod config; pub mod error; pub mod mixnet_client; @@ -18,8 +21,6 @@ pub mod mixnet_listener; mod peer_manager; mod seen_credential_cache; -pub use config::Config; - pub struct OnStartData { // to add more fields as required pub address: Recipient, @@ -33,7 +34,8 @@ impl OnStartData { pub struct Authenticator { #[allow(unused)] - config: crate::node::internal_service_providers::authenticator::Config, + config: Config, + upgrade_mode_state: UpgradeModeDetails, wait_for_gateway: bool, custom_topology_provider: Option>, custom_gateway_transceiver: Option>, @@ -46,7 +48,8 @@ pub struct Authenticator { impl Authenticator { pub fn new( - config: crate::node::internal_service_providers::authenticator::Config, + config: Config, + upgrade_mode_state: UpgradeModeDetails, wireguard_gateway_data: WireguardGatewayData, used_private_network_ips: Vec, ecash_verifier: Arc, @@ -54,6 +57,7 @@ impl Authenticator { ) -> Self { Self { config, + upgrade_mode_state, wait_for_gateway: false, custom_topology_provider: None, custom_gateway_transceiver: None, @@ -152,6 +156,7 @@ impl Authenticator { free_private_network_ips, self.wireguard_gateway_data, mixnet_client, + self.upgrade_mode_state, self.ecash_verifier, ); diff --git a/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs b/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs index fce8cf7174..c057dfa57c 100644 --- a/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs +++ b/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs @@ -19,7 +19,7 @@ impl PeerManager { wireguard_gateway_data, } } - pub async fn add_peer(&mut self, peer: Peer) -> Result<(), AuthenticatorError> { + pub async fn add_peer(&self, peer: Peer) -> Result<(), AuthenticatorError> { let (response_tx, response_rx) = oneshot::channel(); let msg = PeerControlRequest::AddPeer { peer, response_tx }; self.wireguard_gateway_data @@ -38,7 +38,7 @@ impl PeerManager { }) } - pub async fn _remove_peer(&mut self, pub_key: PeerPublicKey) -> Result<(), AuthenticatorError> { + pub async fn _remove_peer(&self, pub_key: PeerPublicKey) -> Result<(), AuthenticatorError> { let key = Key::new(pub_key.to_bytes()); let (response_tx, response_rx) = oneshot::channel(); let msg = PeerControlRequest::RemovePeer { key, response_tx }; @@ -61,7 +61,7 @@ impl PeerManager { } pub async fn query_peer( - &mut self, + &self, public_key: PeerPublicKey, ) -> Result, AuthenticatorError> { let key = Key::new(public_key.to_bytes()); @@ -86,7 +86,7 @@ impl PeerManager { } pub async fn query_bandwidth( - &mut self, + &self, public_key: PeerPublicKey, ) -> Result { let client_bandwidth = self.query_client_bandwidth(public_key).await?; @@ -94,7 +94,7 @@ impl PeerManager { } pub async fn query_client_bandwidth( - &mut self, + &self, key: PeerPublicKey, ) -> Result { let key = Key::new(key.to_bytes()); @@ -121,7 +121,7 @@ impl PeerManager { } pub async fn query_verifier_by_key( - &mut self, + &self, key: PeerPublicKey, credential: CredentialSpendingData, ) -> Result, AuthenticatorError> { @@ -243,7 +243,7 @@ mod tests { Authenticator::default().into(), Arc::new(KeyPair::new(&mut OsRng)), ); - let mut peer_manager = PeerManager::new(wireguard_data); + let peer_manager = PeerManager::new(wireguard_data); let (storage, task_manager) = start_controller( peer_manager.wireguard_gateway_data.peer_tx().clone(), request_rx, diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index e4542b0dd6..7f08a5e52c 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -1,8 +1,10 @@ // Copyright 2020-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::config::Config; use crate::error::GatewayError; use crate::node::client_handling::websocket; +use crate::node::internal_service_providers::authenticator::Authenticator; use crate::node::internal_service_providers::{ authenticator, ExitServiceProviders, ServiceProviderBeingBuilt, SpMessageRouterBuilder, }; @@ -11,6 +13,9 @@ use futures::channel::oneshot; use nym_credential_verification::ecash::{ credential_sender::CredentialHandlerConfig, EcashManager, }; +use nym_credential_verification::upgrade_mode::{ + UpgradeModeCheckConfig, UpgradeModeDetails, UpgradeModeState, +}; use nym_crypto::asymmetric::ed25519; use nym_ip_packet_router::IpPacketRouter; use nym_mixnet_client::forwarder::MixForwardingSender; @@ -30,13 +35,9 @@ use std::sync::Arc; use tracing::*; use zeroize::Zeroizing; -pub(crate) mod client_handling; -pub(crate) mod internal_service_providers; -mod stale_data_cleaner; - -use crate::config::Config; -use crate::node::internal_service_providers::authenticator::Authenticator; +pub use crate::node::upgrade_mode::watcher::UpgradeModeWatcher; pub use client_handling::active_clients::ActiveClientsStore; +pub use nym_credential_verification::upgrade_mode::UpgradeModeCheckRequestSender; pub use nym_gateway_stats_storage::PersistentStatsStorage; pub use nym_gateway_storage::{ error::GatewayStorageError, @@ -45,6 +46,11 @@ pub use nym_gateway_storage::{ }; pub use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent}; +pub(crate) mod client_handling; +pub(crate) mod internal_service_providers; +mod stale_data_cleaner; +pub(crate) mod upgrade_mode; + #[derive(Debug, Clone)] pub struct LocalNetworkRequesterOpts { pub config: nym_network_requester::Config, @@ -78,6 +84,8 @@ pub struct GatewayTasksBuilder { // TODO: combine with authenticator, since you have to start both wireguard_data: Option, + user_agent: UserAgent, + /// ed25519 keypair used to assert one's identity. identity_keypair: Arc, @@ -89,6 +97,8 @@ pub struct GatewayTasksBuilder { metrics: NymNodeMetrics, + upgrade_mode_state: UpgradeModeState, + mnemonic: Arc>, shutdown_tracker: ShutdownTracker, @@ -111,6 +121,8 @@ impl GatewayTasksBuilder { metrics_sender: MetricEventsSender, metrics: NymNodeMetrics, mnemonic: Arc>, + user_agent: UserAgent, + upgrade_mode_attester_public_key: ed25519::PublicKey, shutdown_tracker: ShutdownTracker, ) -> GatewayTasksBuilder { GatewayTasksBuilder { @@ -119,11 +131,13 @@ impl GatewayTasksBuilder { ip_packet_router_opts: None, authenticator_opts: None, wireguard_data: None, + user_agent, identity_keypair: identity, storage, mix_packet_sender, metrics_sender, metrics, + upgrade_mode_state: UpgradeModeState::new(upgrade_mode_attester_public_key), mnemonic, shutdown_tracker, ecash_manager: None, @@ -247,6 +261,7 @@ impl GatewayTasksBuilder { pub async fn build_websocket_listener( &mut self, active_clients_store: ActiveClientsStore, + upgrade_mode_common_state: UpgradeModeDetails, ) -> Result { let shared_state = websocket::CommonHandlerState { cfg: websocket::Config { @@ -261,6 +276,7 @@ impl GatewayTasksBuilder { metrics_sender: self.metrics_sender.clone(), outbound_mix_sender: self.mix_packet_sender.clone(), active_clients_store: active_clients_store.clone(), + upgrade_mode: upgrade_mode_common_state, }; Ok(websocket::Listener::new( @@ -407,6 +423,7 @@ impl GatewayTasksBuilder { pub async fn build_wireguard_authenticator( &mut self, + upgrade_mode_common: UpgradeModeDetails, topology_provider: Box, ) -> Result, GatewayError> { let ecash_manager = self.ecash_manager().await?; @@ -431,6 +448,7 @@ impl GatewayTasksBuilder { let mut authenticator_server = Authenticator::new( opts.config.clone(), + upgrade_mode_common, wireguard_data.inner.clone(), used_private_network_ips, ecash_manager, @@ -462,9 +480,47 @@ impl GatewayTasksBuilder { ) } + pub fn build_upgrade_mode_common_state( + &self, + request_checker: UpgradeModeCheckRequestSender, + ) -> UpgradeModeDetails { + UpgradeModeDetails::new( + UpgradeModeCheckConfig { + min_staleness_recheck: self.config.debug.upgrade_mode_min_staleness_recheck, + }, + request_checker, + self.upgrade_mode_state.clone(), + ) + } + + pub fn try_build_upgrade_mode_watcher(&self) -> Option { + if !self.config.upgrade_mode_watcher.enabled { + warn!("upgrade mode watcher is disabled"); + return None; + } + + Some(UpgradeModeWatcher::new( + self.config + .upgrade_mode_watcher + .debug + .regular_polling_interval, + self.config + .upgrade_mode_watcher + .debug + .expedited_poll_interval, + self.config.debug.upgrade_mode_min_staleness_recheck, + self.config.upgrade_mode_watcher.attestation_url.clone(), + self.upgrade_mode_state.clone(), + self.user_agent.clone(), + self.shutdown_tracker.clone_shutdown_token(), + )) + } + #[cfg(not(target_os = "linux"))] + #[allow(clippy::unimplemented)] pub async fn try_start_wireguard( &mut self, + _upgrade_mode_details: UpgradeModeDetails, ) -> Result, Box> { let _ = self.metrics.clone(); let _ = self.shutdown_tracker.clone(); @@ -474,6 +530,7 @@ impl GatewayTasksBuilder { #[cfg(target_os = "linux")] pub async fn try_start_wireguard( &mut self, + upgrade_mode_details: UpgradeModeDetails, ) -> Result< nym_wireguard_private_metadata_server::ShutdownHandles, Box, @@ -497,6 +554,7 @@ impl GatewayTasksBuilder { nym_wireguard_private_metadata_server::PeerControllerTransceiver::new( wireguard_data.inner.peer_tx().clone(), ), + upgrade_mode_details, )); let bind_address = std::net::SocketAddr::new( @@ -508,6 +566,7 @@ impl GatewayTasksBuilder { ecash_manager, self.metrics.clone(), all_peers, + self.upgrade_mode_state.upgrade_mode_status(), self.shutdown_tracker.clone_shutdown_token(), wireguard_data, ) diff --git a/gateway/src/node/upgrade_mode/mod.rs b/gateway/src/node/upgrade_mode/mod.rs new file mode 100644 index 0000000000..c8df59133b --- /dev/null +++ b/gateway/src/node/upgrade_mode/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod watcher; diff --git a/gateway/src/node/upgrade_mode/watcher.rs b/gateway/src/node/upgrade_mode/watcher.rs new file mode 100644 index 0000000000..c798ddc55b --- /dev/null +++ b/gateway/src/node/upgrade_mode/watcher.rs @@ -0,0 +1,146 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::UserAgent; +use futures::channel::mpsc::unbounded; +use futures::StreamExt; +use nym_credential_verification::upgrade_mode::{ + CheckRequest, UpgradeModeCheckRequestReceiver, UpgradeModeCheckRequestSender, UpgradeModeState, +}; +use nym_task::ShutdownToken; +use nym_upgrade_mode_check::attempt_retrieve_attestation; +use std::time::Duration; +use tokio::task::JoinHandle; +use tokio::time::Instant; +use tracing::{debug, error, info, trace}; +use url::Url; + +pub struct UpgradeModeWatcher { + // default polling interval + regular_polling_interval: Duration, + + // expedited polling interval once upgrade mode is detected + expedited_poll_interval: Duration, + + min_staleness_recheck: Duration, + + attestation_url: Url, + + check_request_sender: UpgradeModeCheckRequestSender, + + check_request_receiver: UpgradeModeCheckRequestReceiver, + + upgrade_mode_state: UpgradeModeState, + + user_agent: UserAgent, + + shutdown_token: ShutdownToken, +} + +impl UpgradeModeWatcher { + pub(crate) fn new( + regular_polling_interval: Duration, + expedited_poll_interval: Duration, + min_staleness_recheck: Duration, + attestation_url: Url, + upgrade_mode_state: UpgradeModeState, + user_agent: UserAgent, + shutdown_token: ShutdownToken, + ) -> Self { + let (tx, rx) = unbounded(); + UpgradeModeWatcher { + regular_polling_interval, + expedited_poll_interval, + min_staleness_recheck, + attestation_url, + check_request_sender: UpgradeModeCheckRequestSender::new(tx), + check_request_receiver: rx, + upgrade_mode_state, + user_agent, + shutdown_token, + } + } + + pub fn request_sender(&self) -> UpgradeModeCheckRequestSender { + self.check_request_sender.clone() + } + + async fn try_update_state(&self) { + match attempt_retrieve_attestation( + self.attestation_url.as_str(), + Some(self.user_agent.clone()), + ) + .await + { + Err(err) => { + info!("upgrade mode attestation is not available at this time"); + debug!("retrieval error: {err}") + } + Ok(attestation) => { + self.upgrade_mode_state + .try_set_expected_attestation(attestation) + .await + } + } + } + + fn timer_reset_deadline(&self) -> Instant { + if self.upgrade_mode_state.upgrade_mode_enabled() { + Instant::now() + self.expedited_poll_interval + } else { + Instant::now() + self.regular_polling_interval + } + } + + async fn handle_check_request(&mut self, polled_request: CheckRequest) { + let mut requests = vec![polled_request]; + while let Ok(Some(queued_up)) = self.check_request_receiver.try_next() { + requests.push(queued_up); + } + + if self.upgrade_mode_state.since_last_query() > self.min_staleness_recheck { + self.try_update_state().await; + } + + for request in requests { + request.finalize() + } + } + + async fn run(&mut self) { + info!("starting the update mode watcher"); + + let check_wait = tokio::time::sleep(self.regular_polling_interval); + tokio::pin!(check_wait); + + loop { + tokio::select! { + biased; + _ = self.shutdown_token.cancelled() => { + trace!("UpdateModeWatcher: received shutdown"); + break; + } + polled_request = self.check_request_receiver.next() => { + let Some(request) = polled_request else { + // this should NEVER happen as `UpgradeModeWatcher` itself holds one sender instance + // but just in case, don't blow up + error!("UpgradeModeCheckRequestReceiver is finished even though we still hold one of the senders!"); + break; + }; + self.handle_check_request(request).await + } + + _ = &mut check_wait => { + self.try_update_state().await; + check_wait.as_mut().reset(self.timer_reset_deadline()); + } + } + } + + debug!("UpdateModeWatcher: Exiting"); + } + + pub fn start(mut self) -> JoinHandle<()> { + tokio::spawn(async move { self.run().await }) + } +} diff --git a/nym-authenticator-client/src/error.rs b/nym-authenticator-client/src/error.rs index 43598acdd8..142fbe6981 100644 --- a/nym-authenticator-client/src/error.rs +++ b/nym-authenticator-client/src/error.rs @@ -42,12 +42,15 @@ pub enum AuthenticationClientError { #[error("unknown authenticator version number")] UnsupportedAuthenticatorVersion, + + #[error("encountered an internal error")] + InternalError, } #[derive(thiserror::Error, Debug)] pub enum RegistrationError { #[error(transparent)] - NoCredentialSent(AuthenticationClientError), // This intentionnally doesn't use `from` to avoid random ? operator to land here when they shouldn't + NoCredentialSent(AuthenticationClientError), // This intentionally doesn't use `from` to avoid random ? operator to land here when they shouldn't #[error("an error occured after a credential was sent : {source}")] CredentialSent { diff --git a/nym-authenticator-client/src/lib.rs b/nym-authenticator-client/src/lib.rs index ab811ef79d..10b651b184 100644 --- a/nym-authenticator-client/src/lib.rs +++ b/nym-authenticator-client/src/lib.rs @@ -12,18 +12,21 @@ use tracing::{debug, error, trace}; use crate::error::Result; use crate::mixnet_listener::{MixnetMessageBroadcastReceiver, MixnetMessageInputSender}; +use crate::types::{AvailableBandwidthClientResponse, TopUpClientResponse}; +use nym_authenticator_requests::traits::UpgradeModeStatus; use nym_authenticator_requests::{ AuthenticatorVersion, client_message::ClientMessage, response::AuthenticatorResponse, - traits::Id, v2, v3, v4, v5, + traits::Id, v2, v3, v4, v5, v6, }; use nym_credentials_interface::{CredentialSpendingData, TicketType}; -use nym_sdk::mixnet::{IncludedSurbs, Recipient}; +use nym_sdk::mixnet::{IncludedSurbs, Recipient, ReconstructedMessage}; use nym_service_provider_requests_common::{Protocol, ServiceProviderTypeExt}; use nym_wireguard_types::PeerPublicKey; mod error; mod helpers; mod mixnet_listener; +pub mod types; pub use crate::error::{AuthenticationClientError, RegistrationError}; pub use crate::mixnet_listener::{AuthClientMixnetListener, AuthClientMixnetListenerHandle}; @@ -61,6 +64,10 @@ impl AuthenticatorClient { } } + fn peer_public_key(&self) -> PeerPublicKey { + PeerPublicKey::from(self.keypair.public_key().inner()) + } + pub async fn send_and_wait_for_response( &mut self, message: &ClientMessage, @@ -72,7 +79,9 @@ impl AuthenticatorClient { } async fn send_request(&self, message: &ClientMessage) -> Result { - let (data, request_id) = message.bytes(self.our_nym_address)?; + let serialised = message.bytes(self.our_nym_address)?; + let data = serialised.bytes; + let request_id = serialised.request_id; // We use 20 surbs for the connect request because typically the // authenticator mixnet client on the nym-node is configured to have a min @@ -96,6 +105,81 @@ impl AuthenticatorClient { Ok(request_id) } + fn handle_response( + &self, + msg: Arc, + request_id: u64, + ) -> Option> { + let Some(header) = msg.message.first_chunk::<2>() else { + debug!( + "received too short message that couldn't have been from the authenticator while waiting for connect response" + ); + return None; + }; + + let Ok(protocol) = Protocol::try_from(header) else { + debug!( + "received a message not meant to any service provider while waiting for connect response" + ); + return None; + }; + + if !protocol.service_provider_type.is_authenticator() { + debug!("Received non-authenticator message while waiting for connect response"); + return None; + } + // Confirm that the version is correct + let version = AuthenticatorVersion::from(protocol.version); + + // Then we deserialize the message + debug!( + "AuthClient: got message while waiting for connect response with version {version:?}" + ); + let ret: Result = match version { + AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => { + return Some(Err( + AuthenticationClientError::UnsupportedAuthenticatorVersion, + )); + } + AuthenticatorVersion::V2 => { + v2::response::AuthenticatorResponse::from_reconstructed_message(&msg) + .map(Into::into) + .map_err(Into::into) + } + AuthenticatorVersion::V3 => { + v3::response::AuthenticatorResponse::from_reconstructed_message(&msg) + .map(Into::into) + .map_err(Into::into) + } + AuthenticatorVersion::V4 => { + v4::response::AuthenticatorResponse::from_reconstructed_message(&msg) + .map(Into::into) + .map_err(Into::into) + } + AuthenticatorVersion::V5 => { + v5::response::AuthenticatorResponse::from_reconstructed_message(&msg) + .map(Into::into) + .map_err(Into::into) + } + AuthenticatorVersion::V6 => { + v6::response::AuthenticatorResponse::from_reconstructed_message(&msg) + .map(Into::into) + .map_err(Into::into) + } + }; + let Ok(response) = ret else { + // This is ok, it's likely just one of our self-pings + debug!("Failed to deserialize reconstructed message"); + return None; + }; + + if response.id() == request_id { + debug!("Got response with matching id"); + return Some(Ok(response)); + } + None + } + async fn listen_for_response(&mut self, request_id: u64) -> Result { let timeout = tokio::time::sleep(Duration::from_secs(10)); tokio::pin!(timeout); @@ -111,42 +195,9 @@ impl AuthenticatorClient { return Err(AuthenticationClientError::NoMixnetMessagesReceived); } Ok(msg) => { - let Some(header) = msg.message.first_chunk::<2>() else { - debug!("received too short message that couldn't have been from the authenticator while waiting for connect response"); - continue; - }; - - let Ok(protocol) = Protocol::try_from(header) else { - debug!("received a message not meant to any service provider while waiting for connect response"); - continue; - }; - - if !protocol.service_provider_type.is_authenticator() { - debug!("Received non-authenticator message while waiting for connect response"); - continue; - } - // Confirm that the version is correct - let version = AuthenticatorVersion::from(protocol.version); - - // Then we deserialize the message - debug!("AuthClient: got message while waiting for connect response with version {version:?}"); - let ret: Result = match version { - AuthenticatorVersion::V1 => Err(AuthenticationClientError::UnsupportedVersion), - AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), - AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), - AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), - AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), - AuthenticatorVersion::UNKNOWN => Err(AuthenticationClientError::UnknownVersion), - }; - let Ok(response) = ret else { - // This is ok, it's likely just one of our self-pings - debug!("Failed to deserialize reconstructed message"); - continue; - }; - - if response.id() == request_id { - debug!("Got response with matching id"); - return Ok(response); + match self.handle_response(msg, request_id) { + None => continue, + Some(res) => return res, } } } @@ -160,6 +211,8 @@ impl AuthenticatorClient { ticketbook_type: TicketType, ) -> std::result::Result { debug!("Registering with the wg gateway..."); + let pub_key = self.peer_public_key(); + let init_message = match self.auth_version { AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => { return Err(RegistrationError::NoCredentialSent( @@ -167,24 +220,19 @@ impl AuthenticatorClient { )); } AuthenticatorVersion::V2 => { - ClientMessage::Initial(Box::new(v2::registration::InitMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - })) + ClientMessage::Initial(Box::new(v2::registration::InitMessage { pub_key })) } AuthenticatorVersion::V3 => { - ClientMessage::Initial(Box::new(v3::registration::InitMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - })) + ClientMessage::Initial(Box::new(v3::registration::InitMessage { pub_key })) } AuthenticatorVersion::V4 => { - ClientMessage::Initial(Box::new(v4::registration::InitMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - })) + ClientMessage::Initial(Box::new(v4::registration::InitMessage { pub_key })) } AuthenticatorVersion::V5 => { - ClientMessage::Initial(Box::new(v5::registration::InitMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - })) + ClientMessage::Initial(Box::new(v5::registration::InitMessage { pub_key })) + } + AuthenticatorVersion::V6 => { + ClientMessage::Initial(Box::new(v6::registration::InitMessage { pub_key })) } }; trace!("sending init msg to {}: {:?}", &self.ip_addr, &init_message); @@ -224,65 +272,22 @@ impl AuthenticatorClient { })? .data, ); + let credential = credential + .map(TryInto::try_into) + .transpose() + .inspect_err(|err| error!("failed to convert {ticketbook_type} ticket to a valid BandwidthClaim: {err}")) + .map_err(|_| RegistrationError::CredentialSent { + source: AuthenticationClientError::InternalError, + })?; - let finalized_message = match self.auth_version { - AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => { - return Err(RegistrationError::CredentialSent { - source: AuthenticationClientError::UnsupportedAuthenticatorVersion, - }); - } - AuthenticatorVersion::V2 => { - ClientMessage::Final(Box::new(v2::registration::FinalMessage { - gateway_client: v2::registration::GatewayClient::new( - self.keypair.private_key(), - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips().ipv4.into(), - pending_registration_response.nonce(), - ), - credential, - })) - } - AuthenticatorVersion::V3 => { - ClientMessage::Final(Box::new(v3::registration::FinalMessage { - gateway_client: v3::registration::GatewayClient::new( - self.keypair.private_key(), - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips().ipv4.into(), - pending_registration_response.nonce(), - ), - credential, - })) - } - AuthenticatorVersion::V4 => { - ClientMessage::Final(Box::new(v4::registration::FinalMessage { - gateway_client: v4::registration::GatewayClient::new( - self.keypair.private_key(), - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips().into(), - pending_registration_response.nonce(), - ), - credential, - })) - } - AuthenticatorVersion::V5 => { - ClientMessage::Final(Box::new(v5::registration::FinalMessage { - gateway_client: v5::registration::GatewayClient::new( - self.keypair.private_key(), - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips(), - pending_registration_response.nonce(), - ), - credential, - })) - } - }; - trace!( - "sending final msg to {}: {:?}", - &self.ip_addr, &finalized_message - ); + let finalized_message = pending_registration_response + .finalise_registration(self.keypair.private_key(), credential); + let client_message = ClientMessage::Final(finalized_message); + + trace!("sending final msg to {}: {client_message:?}", &self.ip_addr); let response = self - .send_and_wait_for_response(&finalized_message) + .send_and_wait_for_response(&client_message) .await .map_err(|source| RegistrationError::CredentialSent { source })?; let AuthenticatorResponse::Registered(registered_response) = response else { @@ -316,32 +321,24 @@ impl AuthenticatorClient { } // This is up to the caller to know nothing is ever spent there - pub async fn query_bandwidth(&mut self) -> Result> { + pub async fn query_bandwidth(&mut self) -> Result { + let pub_key = self.peer_public_key(); + let version = self.auth_version; + let query_message = match self.auth_version { - AuthenticatorVersion::V1 => { + AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => { return Err(AuthenticationClientError::UnsupportedAuthenticatorVersion); } - AuthenticatorVersion::V2 => ClientMessage::Query(Box::new(QueryMessageImpl { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - version: AuthenticatorVersion::V2, - })), - AuthenticatorVersion::V3 => ClientMessage::Query(Box::new(QueryMessageImpl { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - version: AuthenticatorVersion::V3, - })), - AuthenticatorVersion::V4 => ClientMessage::Query(Box::new(QueryMessageImpl { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - version: AuthenticatorVersion::V4, - })), - AuthenticatorVersion::V5 => ClientMessage::Query(Box::new(QueryMessageImpl { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - version: AuthenticatorVersion::V5, - })), - AuthenticatorVersion::UNKNOWN => { - return Err(AuthenticationClientError::UnsupportedAuthenticatorVersion); + AuthenticatorVersion::V2 + | AuthenticatorVersion::V3 + | AuthenticatorVersion::V4 + | AuthenticatorVersion::V5 + | AuthenticatorVersion::V6 => { + ClientMessage::Query(Box::new(QueryMessageImpl { pub_key, version })) } }; let response = self.send_and_wait_for_response(&query_message).await?; + let current_upgrade_mode_status = response.upgrade_mode_status(); let available_bandwidth = match response { AuthenticatorResponse::RemainingBandwidth(remaining_bandwidth_response) => { @@ -350,7 +347,10 @@ impl AuthenticatorClient { { available_bandwidth } else { - return Ok(None); + return Ok(AvailableBandwidthClientResponse { + available_bandwidth_bytes: None, + current_upgrade_mode_status, + }); } } _ => return Err(AuthenticationClientError::InvalidGatewayAuthResponse), @@ -371,24 +371,35 @@ impl AuthenticatorClient { "Remaining bandwidth is under 1 MB. The wireguard mode will get suspended after that until tomorrow, UTC time. The client might shutdown with timeout soon" ); } - Ok(Some(available_bandwidth)) + Ok(AvailableBandwidthClientResponse { + available_bandwidth_bytes: Some(available_bandwidth), + current_upgrade_mode_status, + }) } // Since the caller provides the credential, it knows it is spent - pub async fn top_up(&mut self, credential: CredentialSpendingData) -> Result { + pub async fn top_up( + &mut self, + credential: CredentialSpendingData, + ) -> Result { + let pub_key = self.peer_public_key(); let top_up_message = match self.auth_version { AuthenticatorVersion::V3 => ClientMessage::TopUp(Box::new(v3::topup::TopUpMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + pub_key, credential, })), // NOTE: looks like a bug here using v3. But we're leaving it as is since it's working // and V4 is deprecated in favour of V5 AuthenticatorVersion::V4 => ClientMessage::TopUp(Box::new(v4::topup::TopUpMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + pub_key, credential, })), AuthenticatorVersion::V5 => ClientMessage::TopUp(Box::new(v5::topup::TopUpMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + pub_key, + credential, + })), + AuthenticatorVersion::V6 => ClientMessage::TopUp(Box::new(v6::topup::TopUpMessage { + pub_key, credential, })), AuthenticatorVersion::V1 | AuthenticatorVersion::V2 | AuthenticatorVersion::UNKNOWN => { @@ -396,14 +407,18 @@ impl AuthenticatorClient { } }; let response = self.send_and_wait_for_response(&top_up_message).await?; + let current_upgrade_mode_status = response.upgrade_mode_status(); - let remaining_bandwidth = match response { + let remaining_bandwidth_bytes = match response { AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => { top_up_bandwidth_response.available_bandwidth() } _ => return Err(AuthenticationClientError::InvalidGatewayAuthResponse), }; - Ok(remaining_bandwidth) + Ok(TopUpClientResponse { + remaining_bandwidth_bytes, + current_upgrade_mode_status, + }) } } diff --git a/nym-authenticator-client/src/types.rs b/nym-authenticator-client/src/types.rs new file mode 100644 index 0000000000..6ed3188fd0 --- /dev/null +++ b/nym-authenticator-client/src/types.rs @@ -0,0 +1,16 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub use nym_authenticator_requests::models::CurrentUpgradeModeStatus; + +#[derive(Debug, Clone, Copy)] +pub struct TopUpClientResponse { + pub remaining_bandwidth_bytes: i64, + pub current_upgrade_mode_status: CurrentUpgradeModeStatus, +} + +#[derive(Debug, Clone, Copy)] +pub struct AvailableBandwidthClientResponse { + pub available_bandwidth_bytes: Option, + pub current_upgrade_mode_status: CurrentUpgradeModeStatus, +} diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/lib.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/lib.rs index 6ff65c9fda..54ba8126b5 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/src/lib.rs +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/lib.rs @@ -5,8 +5,6 @@ pub mod api; pub mod client; mod helpers; -pub const CREDENTIAL_PROXY_JWT_ISSUER: &str = "nym-credential-proxy"; - macro_rules! absolute_route { ( $name:ident, $parent:expr, $suffix:expr ) => { pub fn $name() -> String { diff --git a/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs b/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs index e0ff97ac05..b544f80792 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs @@ -21,6 +21,8 @@ pub struct AttestationWatcher { attestation_url: Url, + expected_attester_public_key: ed25519::PublicKey, + jwt_signing_keys: ed25519::KeyPair, jwt_validity: Duration, @@ -32,6 +34,7 @@ impl AttestationWatcher { pub(crate) fn new( regular_polling_interval: Duration, expedited_poll_interval: Duration, + expected_attester_public_key: ed25519::PublicKey, attestation_url: Url, jwt_signing_keys: ed25519::KeyPair, jwt_validity: Duration, @@ -40,6 +43,7 @@ impl AttestationWatcher { regular_polling_interval, expedited_poll_interval, attestation_url, + expected_attester_public_key, jwt_signing_keys, jwt_validity, upgrade_mode_state: UpgradeModeState { @@ -65,7 +69,12 @@ impl AttestationWatcher { } Ok(attestation) => { self.upgrade_mode_state - .update(attestation, &self.jwt_signing_keys, self.jwt_validity) + .update( + attestation, + self.expected_attester_public_key, + &self.jwt_signing_keys, + self.jwt_validity, + ) .await } } @@ -74,7 +83,7 @@ impl AttestationWatcher { pub async fn run_forever(self, cancellation_token: CancellationToken) { info!("starting the attestation watcher task"); - let check_wait = tokio::time::sleep(self.regular_polling_interval); + let check_wait = tokio::time::sleep(Duration::new(0, 0)); tokio::pin!(check_wait); loop { diff --git a/nym-credential-proxy/nym-credential-proxy/src/cli.rs b/nym-credential-proxy/nym-credential-proxy/src/cli.rs index d5c27a7730..5ab32daf32 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/cli.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/cli.rs @@ -159,6 +159,10 @@ pub struct UpgradeModeConfig { #[clap(long, env = "NYM_CREDENTIAL_PROXY_ATTESTATION_CHECK_URL")] pub(crate) attestation_check_url: Option, + /// Base58-encoded expected upgrade mode attestation ed25519 public key. + #[clap(long, env = "NYM_CREDENTIAL_PROXY_ATTESTER_PUBKEY")] + pub(crate) attester_pubkey: Option, + /// Default polling interval of the upgrade mode endpoint. #[clap( long, diff --git a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs index cd061d8962..7044112d79 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs @@ -79,6 +79,29 @@ pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> { } }; + let attester_pubkey = match cli.upgrade_mode.attester_pubkey { + Some(pubkey) => pubkey, + None => { + // argument hasn't been provided and env is not configured + if std::env::var(CONFIGURED).is_err() { + return Err(CredentialProxyError::AttesterPublicKeyNotSet); + } + // argument hasn't been provided and the relevant env value hasn't been set + // (technically this shouldn't be possible) + let Ok(env_key) = std::env::var(var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY) + else { + return Err(CredentialProxyError::AttesterPublicKeyNotSet); + }; + + match env_key.parse() { + Ok(key) => key, + Err(err) => { + return Err(CredentialProxyError::MalformedAttesterPublicKey { source: err }); + } + } + } + }; + let ticketbook_manager = TicketbookManager::new( build_sha_short(), cli.quorum_check_interval, @@ -94,6 +117,7 @@ pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> { cli.upgrade_mode.attestation_check_regular_polling_interval, cli.upgrade_mode .attestation_check_expedited_polling_interval, + attester_pubkey, upgrade_mode_attestation_check_url, jwt_signing_keys, cli.upgrade_mode.upgrade_mode_jwt_validity, diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/state/nyx_upgrade_mode.rs b/nym-credential-proxy/nym-credential-proxy/src/http/state/nyx_upgrade_mode.rs index 80c94a055f..eed655d695 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/state/nyx_upgrade_mode.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/state/nyx_upgrade_mode.rs @@ -1,14 +1,15 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_credential_proxy_requests::CREDENTIAL_PROXY_JWT_ISSUER; -use nym_credential_proxy_requests::api::v1::ticketbook::models::UpgradeModeAttestation; use nym_crypto::asymmetric::ed25519; -use nym_upgrade_mode_check::generate_jwt_for_upgrade_mode_attestation; +use nym_upgrade_mode_check::{ + CREDENTIAL_PROXY_JWT_ISSUER, UpgradeModeAttestation, generate_jwt_for_upgrade_mode_attestation, +}; use std::sync::Arc; use std::time::Duration; use time::OffsetDateTime; use tokio::sync::RwLock; +use tracing::error; #[derive(Debug, Clone)] pub(crate) struct UpgradeModeState { @@ -23,6 +24,7 @@ impl UpgradeModeState { pub(crate) async fn update( &self, retrieved_attestation: Option, + expected_attester_public_key: ed25519::PublicKey, jwt_signing_keys: &ed25519::KeyPair, jwt_validity: Duration, ) { @@ -32,6 +34,14 @@ impl UpgradeModeState { return; }; + if attestation.content.attester_public_key != expected_attester_public_key { + error!( + "the retrieved attestation has been signed with an unexpected key! expected pubkey: {} actual: {}", + expected_attester_public_key, attestation.content.attester_public_key + ); + return; + } + match guard.as_mut() { None => { // no existing state - it's the first time we're going into upgrade mode, diff --git a/nym-gateway-probe/src/lib.rs b/nym-gateway-probe/src/lib.rs index cac1225a8c..76b8c5c567 100644 --- a/nym-gateway-probe/src/lib.rs +++ b/nym-gateway-probe/src/lib.rs @@ -16,7 +16,7 @@ use futures::StreamExt; use nym_authenticator_client::{AuthClientMixnetListener, AuthenticatorClient}; use nym_authenticator_requests::{ AuthenticatorVersion, client_message::ClientMessage, response::AuthenticatorResponse, v2, v3, - v4, v5, + v4, v5, v6, }; use nym_client_core::config::ForgetMe; use nym_config::defaults::{ @@ -467,6 +467,7 @@ async fn wg_probe( auth_version: AuthenticatorVersion, awg_args: String, netstack_args: NetstackArgs, + // TODO: update type credential: CredentialSpendingData, ) -> anyhow::Result { info!("attempting to use authenticator version {auth_version:?}"); @@ -493,6 +494,9 @@ async fn wg_probe( AuthenticatorVersion::V5 => ClientMessage::Initial(Box::new( v5::registration::InitMessage::new(authenticator_pub_key), )), + AuthenticatorVersion::V6 => ClientMessage::Initial(Box::new( + v6::registration::InitMessage::new(authenticator_pub_key), + )), AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => bail!("unknown version number"), }; @@ -512,57 +516,17 @@ async fn wg_probe( debug!("Verifying data"); pending_registration_response.verify(&private_key)?; - let finalized_message = match auth_version { - AuthenticatorVersion::V2 => { - ClientMessage::Final(Box::new(v2::registration::FinalMessage { - gateway_client: v2::registration::GatewayClient::new( - &private_key, - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips().ipv4.into(), - pending_registration_response.nonce(), - ), - credential: Some(credential), - })) - } - AuthenticatorVersion::V3 => { - ClientMessage::Final(Box::new(v3::registration::FinalMessage { - gateway_client: v3::registration::GatewayClient::new( - &private_key, - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips().ipv4.into(), - pending_registration_response.nonce(), - ), - credential: Some(credential), - })) - } - AuthenticatorVersion::V4 => { - ClientMessage::Final(Box::new(v4::registration::FinalMessage { - gateway_client: v4::registration::GatewayClient::new( - &private_key, - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips().into(), - pending_registration_response.nonce(), - ), - credential: Some(credential), - })) - } - AuthenticatorVersion::V5 => { - ClientMessage::Final(Box::new(v5::registration::FinalMessage { - gateway_client: v5::registration::GatewayClient::new( - &private_key, - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips(), - pending_registration_response.nonce(), - ), - credential: Some(credential), - })) - } - AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => { - bail!("Unknown version number") - } - }; + let credential = credential + .try_into() + .inspect_err(|err| error!("invalid zk-nym data: {err}")) + .ok(); + + let finalized_message = + pending_registration_response.finalise_registration(&private_key, credential); + let client_message = ClientMessage::Final(finalized_message); + let response = auth_client - .send_and_wait_for_response(&finalized_message) + .send_and_wait_for_response(&client_message) .await?; let AuthenticatorResponse::Registered(registered_response) = response else { bail!("Unexpected response"); diff --git a/nym-node/src/cli/commands/run/args.rs b/nym-node/src/cli/commands/run/args.rs index 36117d0ba6..8c214f5e5e 100644 --- a/nym-node/src/cli/commands/run/args.rs +++ b/nym-node/src/cli/commands/run/args.rs @@ -159,7 +159,7 @@ impl Args { name: "id".to_string(), })?; - let config = ConfigBuilder::new(id, config_path.clone(), data_dir.clone()) + ConfigBuilder::new(id, config_path.clone(), data_dir.clone()) // the old default behaviour of running in mixnode mode if nothing is explicitly set .with_modes( self.custom_modes() @@ -172,11 +172,9 @@ impl Args { .with_storage_paths(NymNodePaths::new(&data_dir)) .with_verloc(self.verloc.build_config_section()) .with_metrics(self.metrics.build_config_section()) - .with_gateway_tasks(self.entry_gateway.build_config_section(&data_dir)) + .with_gateway_tasks(self.entry_gateway.build_config_section(&data_dir)?) .with_service_providers(self.exit_gateway.build_config_section(&data_dir)) - .build(); - - Ok(config) + .build() } pub(crate) fn override_config(self, mut config: Config) -> Config { diff --git a/nym-node/src/cli/helpers.rs b/nym-node/src/cli/helpers.rs index 5211696485..e7eb81623f 100644 --- a/nym-node/src/cli/helpers.rs +++ b/nym-node/src/cli/helpers.rs @@ -5,6 +5,7 @@ use super::DEFAULT_NYMNODE_ID; use crate::config; use crate::config::default_config_filepath; use crate::env::vars::*; +use crate::error::NymNodeError; use celes::Country; use clap::Args; use clap::builder::ArgPredicate; @@ -426,6 +427,14 @@ pub(crate) struct EntryGatewayArgs { env = NYMNODE_MNEMONIC_ARG )] pub(crate) mnemonic: Option, + + /// Endpoint to query to retrieve current upgrade mode attestation. + #[clap( + long, + env = NYMNODE_UPGRADE_MODE_ATTESTATION_URL_ARG + )] + #[zeroize(skip)] + pub(crate) upgrade_mode_attestation_url: Option, } impl EntryGatewayArgs { @@ -433,12 +442,12 @@ impl EntryGatewayArgs { pub(crate) fn build_config_section>( self, data_dir: P, - ) -> config::GatewayTasksConfig { - self.override_config_section(config::GatewayTasksConfig::new_default(data_dir)) + ) -> Result { + Ok(self.override_config_section(config::GatewayTasksConfig::new(data_dir)?)) } pub(crate) fn override_config_section( - self, + mut self, mut section: config::GatewayTasksConfig, ) -> config::GatewayTasksConfig { if let Some(bind_address) = self.entry_bind_address { @@ -453,6 +462,9 @@ impl EntryGatewayArgs { if let Some(enforce_zk_nyms) = self.enforce_zk_nyms { section.enforce_zk_nyms = enforce_zk_nyms } + if let Some(upgrade_mode_attestation_url) = self.upgrade_mode_attestation_url.take() { + section.upgrade_mode.attestation_url = upgrade_mode_attestation_url + } section } diff --git a/nym-node/src/config/gateway_tasks.rs b/nym-node/src/config/gateway_tasks.rs index 0fcf54d9dd..16517b77bc 100644 --- a/nym-node/src/config/gateway_tasks.rs +++ b/nym-node/src/config/gateway_tasks.rs @@ -1,14 +1,22 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::config::helpers::log_error_and_return; use crate::config::persistence::GatewayTasksPaths; -use nym_config::defaults::{DEFAULT_CLIENT_LISTENING_PORT, TICKETBOOK_VALIDITY_DAYS}; +use crate::error::NymNodeError; +use nym_config::defaults::{ + DEFAULT_CLIENT_LISTENING_PORT, TICKETBOOK_VALIDITY_DAYS, mainnet, var_names, +}; use nym_config::helpers::in6addr_any_init; use nym_config::serde_helpers::de_maybe_port; +use nym_crypto::asymmetric::ed25519::{self, serde_helpers::bs58_ed25519_pubkey}; use serde::{Deserialize, Serialize}; +use std::env; use std::net::SocketAddr; use std::path::Path; use std::time::Duration; +use tracing::info; +use url::Url; pub const DEFAULT_WS_PORT: u16 = DEFAULT_CLIENT_LISTENING_PORT; @@ -36,6 +44,8 @@ pub struct GatewayTasksConfig { #[serde(deserialize_with = "de_maybe_port")] pub announce_wss_port: Option, + pub upgrade_mode: UpgradeModeWatcher, + #[serde(default)] pub debug: Debug, } @@ -63,6 +73,10 @@ pub struct Debug { pub client_bandwidth: ClientBandwidthDebug, pub zk_nym_tickets: ZkNymTicketHandlerDebug, + + /// The minimum duration since the last explicit check for the upgrade mode to allow creation of new requests. + #[serde(with = "humantime_serde")] + pub upgrade_mode_min_staleness_recheck: Duration, } impl Debug { @@ -70,6 +84,7 @@ impl Debug { pub const DEFAULT_MINIMUM_MIX_PERFORMANCE: u8 = 50; pub const DEFAULT_MAXIMUM_AUTH_REQUEST_TIMESTAMP_SKEW: Duration = Duration::from_secs(120); pub const DEFAULT_MAXIMUM_OPEN_CONNECTIONS: usize = 8192; + pub const DEFAULT_UPGRADE_MODE_MIN_STALENESS_RECHECK: Duration = Duration::from_secs(30); } impl Default for Debug { @@ -82,6 +97,7 @@ impl Default for Debug { stale_messages: Default::default(), client_bandwidth: Default::default(), zk_nym_tickets: Default::default(), + upgrade_mode_min_staleness_recheck: Self::DEFAULT_UPGRADE_MODE_MIN_STALENESS_RECHECK, } } } @@ -201,14 +217,149 @@ impl Default for StaleMessageDebug { } impl GatewayTasksConfig { - pub fn new_default>(data_dir: P) -> Self { - GatewayTasksConfig { + pub fn new>(data_dir: P) -> Result { + Ok(GatewayTasksConfig { storage_paths: GatewayTasksPaths::new(data_dir), enforce_zk_nyms: false, ws_bind_address: SocketAddr::new(in6addr_any_init(), DEFAULT_WS_PORT), announce_ws_port: None, announce_wss_port: None, + upgrade_mode: UpgradeModeWatcher::new()?, debug: Default::default(), + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpgradeModeWatcher { + /// Specifies whether this gateway watches for upgrade mode changes + /// via the published attestation file. + pub enabled: bool, + + /// Endpoint to query to retrieve current upgrade mode attestation. + pub attestation_url: Url, + + /// Expected public key of the attester providing the upgrade mode attestation + /// on the specified endpoint + #[serde(with = "bs58_ed25519_pubkey")] + pub attester_public_key: ed25519::PublicKey, + + pub debug: UpgradeModeWatcherDebug, +} + +impl From for nym_gateway::config::UpgradeModeWatcher { + fn from(config: UpgradeModeWatcher) -> Self { + nym_gateway::config::UpgradeModeWatcher { + enabled: config.enabled, + attestation_url: config.attestation_url, + debug: nym_gateway::config::UpgradeModeWatcherDebug { + regular_polling_interval: config.debug.regular_polling_interval, + expedited_poll_interval: config.debug.expedited_poll_interval, + }, + } + } +} + +impl UpgradeModeWatcher { + pub fn new_mainnet() -> UpgradeModeWatcher { + info!("using mainnet configuration for the upgrade mode:"); + info!("\t- url: {}", mainnet::UPGRADE_MODE_ATTESTATION_URL); + info!( + "\t- attester public key: {}", + mainnet::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY + ); + + // SAFETY: + // our hardcoded values should always be valid + #[allow(clippy::expect_used)] + let attestation_url = mainnet::UPGRADE_MODE_ATTESTATION_URL + .parse() + .expect("invalid default upgrade mode attestation URL"); + + #[allow(clippy::expect_used)] + let attester_public_key = mainnet::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY + .parse() + .expect("invalid default upgrade mode attester public key"); + + UpgradeModeWatcher { + enabled: true, + attestation_url, + attester_public_key, + debug: UpgradeModeWatcherDebug::default(), + } + } + + pub fn new() -> Result { + // if env is configured, extract relevant values from there, otherwise fallback to mainnet + if env::var(var_names::CONFIGURED).is_err() { + return Ok(Self::new_mainnet()); + } + + // if env is configured, the relevant values should be set + let Ok(env_attestation_url) = env::var(var_names::UPGRADE_MODE_ATTESTATION_URL) else { + return log_error_and_return(format!( + "'{}' is not set whilst the env is set to be configured", + var_names::UPGRADE_MODE_ATTESTATION_URL + )); + }; + + let Ok(env_attester_pubkey) = + env::var(var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY) + else { + return log_error_and_return(format!( + "'{}' is not set whilst the env is set to be configured", + var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY + )); + }; + + let attestation_url = match env_attestation_url.parse() { + Ok(url) => url, + Err(err) => { + return log_error_and_return(format!( + "provided attestation url {env_attestation_url} is invalid: {err}!" + )); + } + }; + + let attester_public_key = match env_attester_pubkey.parse() { + Ok(public_key) => public_key, + Err(err) => { + return log_error_and_return(format!( + "provided attester public key {env_attester_pubkey} is invalid: {err}!" + )); + } + }; + + Ok(UpgradeModeWatcher { + enabled: true, + attestation_url, + attester_public_key, + debug: UpgradeModeWatcherDebug::default(), + }) + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct UpgradeModeWatcherDebug { + /// Default polling interval + #[serde(with = "humantime_serde")] + pub regular_polling_interval: Duration, + + /// Expedited polling interval for once upgrade mode is detected + #[serde(with = "humantime_serde")] + pub expedited_poll_interval: Duration, +} + +impl UpgradeModeWatcherDebug { + const DEFAULT_REGULAR_POLLING_INTERVAL: Duration = Duration::from_secs(15 * 60); + const DEFAULT_EXPEDITED_POLL_INTERVAL: Duration = Duration::from_secs(2 * 60); +} + +impl Default for UpgradeModeWatcherDebug { + fn default() -> Self { + UpgradeModeWatcherDebug { + regular_polling_interval: Self::DEFAULT_REGULAR_POLLING_INTERVAL, + expedited_poll_interval: Self::DEFAULT_EXPEDITED_POLL_INTERVAL, } } } diff --git a/nym-node/src/config/helpers.rs b/nym-node/src/config/helpers.rs index cdbffb0f25..9605302aa2 100644 --- a/nym-node/src/config/helpers.rs +++ b/nym-node/src/config/helpers.rs @@ -3,11 +3,13 @@ use super::LocalWireguardOpts; use crate::config::Config; +use crate::error::NymNodeError; use clap::crate_version; use nym_gateway::node::{ LocalAuthenticatorOpts, LocalIpPacketRouterOpts, LocalNetworkRequesterOpts, }; use nym_gateway::nym_authenticator; +use tracing::error; // a temporary solution until further refactoring is made fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config { @@ -24,6 +26,7 @@ fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config { nym_gateway::config::IpPacketRouter { enabled: config.service_providers.network_requester.debug.enabled, }, + config.gateway_tasks.upgrade_mode.clone(), nym_gateway::config::Debug { client_bandwidth_max_flushing_rate: config .gateway_tasks @@ -62,6 +65,10 @@ fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config { .maximum_time_between_redemption, }, max_request_timestamp_skew: config.gateway_tasks.debug.max_request_timestamp_skew, + upgrade_mode_min_staleness_recheck: config + .gateway_tasks + .debug + .upgrade_mode_min_staleness_recheck, }, ) } @@ -218,3 +225,9 @@ pub fn gateway_tasks_config(config: &Config) -> GatewayTasksConfig { wg_opts, } } + +pub(crate) fn log_error_and_return(msg: impl Into) -> Result { + let msg = msg.into(); + error!("{msg}"); + Err(NymNodeError::config_validation_failure(msg)) +} diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index 3a20022a15..edec2a80af 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -262,8 +262,13 @@ impl ConfigBuilder { self } - pub fn build(self) -> Config { - Config { + pub fn build(self) -> Result { + let gateway_tasks = match self.gateway_tasks { + Some(gateway_tasks) => gateway_tasks, + None => GatewayTasksConfig::new(&self.data_dir)?, + }; + + Ok(Config { id: self.id, modes: self.modes, host: self.host.unwrap_or_default(), @@ -279,16 +284,14 @@ impl ConfigBuilder { .storage_paths .unwrap_or_else(|| NymNodePaths::new(&self.data_dir)), metrics: self.metrics.unwrap_or_default(), - gateway_tasks: self - .gateway_tasks - .unwrap_or_else(|| GatewayTasksConfig::new_default(&self.data_dir)), + gateway_tasks, service_providers: self .service_providers .unwrap_or_else(|| ServiceProvidersConfig::new_default(&self.data_dir)), logging: self.logging.unwrap_or_default(), save_path: Some(self.config_path), debug: Default::default(), - } + }) } } diff --git a/nym-node/src/config/old_configs/old_config_v10.rs b/nym-node/src/config/old_configs/old_config_v10.rs index 3a395685f5..e45cca8dd2 100644 --- a/nym-node/src/config/old_configs/old_config_v10.rs +++ b/nym-node/src/config/old_configs/old_config_v10.rs @@ -3,7 +3,7 @@ use crate::config::authenticator::{Authenticator, AuthenticatorDebug}; use crate::config::gateway_tasks::{ - ClientBandwidthDebug, StaleMessageDebug, ZkNymTicketHandlerDebug, + ClientBandwidthDebug, StaleMessageDebug, UpgradeModeWatcher, ZkNymTicketHandlerDebug, }; use crate::config::persistence::{ AuthenticatorPaths, GatewayTasksPaths, IpPacketRouterPaths, KeysPaths, NetworkRequesterPaths, @@ -33,7 +33,7 @@ use serde::{Deserialize, Serialize}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::path::{Path, PathBuf}; use std::time::Duration; -use tracing::{debug, instrument}; +use tracing::{debug, error, instrument}; use url::Url; #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] @@ -1346,6 +1346,13 @@ pub async fn try_upgrade_config_v10>( ws_bind_address: old_cfg.gateway_tasks.ws_bind_address, announce_ws_port: old_cfg.gateway_tasks.announce_ws_port, announce_wss_port: old_cfg.gateway_tasks.announce_wss_port, + upgrade_mode: UpgradeModeWatcher::new() + .inspect_err(|_| { + error!( + "failed to set custom upgrade mode configuration - falling back to mainnet" + ) + }) + .unwrap_or(UpgradeModeWatcher::new_mainnet()), debug: gateway_tasks::Debug { message_retrieval_limit: old_cfg.gateway_tasks.debug.message_retrieval_limit, maximum_open_connections: old_cfg.gateway_tasks.debug.maximum_open_connections, @@ -1394,6 +1401,7 @@ pub async fn try_upgrade_config_v10>( .zk_nym_tickets .maximum_time_between_redemption, }, + ..Default::default() }, }, service_providers: ServiceProvidersConfig { diff --git a/nym-node/src/config/template.rs b/nym-node/src/config/template.rs index bf8ed72710..8177119780 100644 --- a/nym-node/src/config/template.rs +++ b/nym-node/src/config/template.rs @@ -201,6 +201,18 @@ announce_ws_port = {{#if gateway_tasks.announce_ws_port }} {{ gateway_tasks.anno # (default: 0 - disabled) announce_wss_port = {{#if gateway_tasks.announce_wss_port }} {{ gateway_tasks.announce_wss_port }} {{else}} 0 {{/if}} +[gateway_tasks.upgrade_mode] +# Specifies whether this gateway watches for upgrade mode changes +# via the published attestation file. +enabled = {{ gateway_tasks.upgrade_mode.enabled }} + +# Endpoint to query to retrieve current upgrade mode attestation. +# If not provided, it implicitly disables the watcher and upgrade-mode features +attestation_url = '{{ gateway_tasks.upgrade_mode.attestation_url }}' + +# Expected public key of the attester providing the upgrade mode attestation +# on the specified endpoint +attester_public_key = '{{ gateway_tasks.upgrade_mode.attester_public_key }}' [gateway_tasks.storage_paths] # Path to sqlite database containing all persistent data: messages for offline clients, diff --git a/nym-node/src/env.rs b/nym-node/src/env.rs index 13aedc7c0a..8f9a6d65b8 100644 --- a/nym-node/src/env.rs +++ b/nym-node/src/env.rs @@ -61,6 +61,8 @@ pub mod vars { pub const NYMNODE_ENTRY_ANNOUNCE_WSS_PORT_ARG: &str = "NYMNODE_ENTRY_ANNOUNCE_WSS_PORT"; pub const NYMNODE_ENFORCE_ZK_NYMS_ARG: &str = "NYMNODE_ENFORCE_ZK_NYMS"; pub const NYMNODE_MNEMONIC_ARG: &str = "NYMNODE_MNEMONIC"; + pub const NYMNODE_UPGRADE_MODE_ATTESTATION_URL_ARG: &str = + "NYMNODE_UPGRADE_MODE_ATTESTATION_URL"; // exit gateway: pub const NYMNODE_UPSTREAM_EXIT_POLICY_ARG: &str = "NYMNODE_UPSTREAM_EXIT_POLICY"; diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 44d35b5d41..c0f94eb6da 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -40,7 +40,7 @@ use crate::node::shared_network::{ }; use nym_bin_common::bin_info; use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_gateway::node::{ActiveClientsStore, GatewayTasksBuilder}; +use nym_gateway::node::{ActiveClientsStore, GatewayTasksBuilder, UpgradeModeCheckRequestSender}; use nym_mixnet_client::client::ActiveConnections; use nym_mixnet_client::forwarder::MixForwardingSender; use nym_network_requester::{ @@ -624,9 +624,27 @@ impl NymNode { metrics_sender, self.metrics.clone(), self.entry_gateway.mnemonic.clone(), + Self::user_agent(), + self.config.gateway_tasks.upgrade_mode.attester_public_key, self.shutdown_tracker().clone(), ); + // start task for watching the changes in upgrade mode attestation + let upgrade_check_request_sender = if let Some(upgrade_mode_watcher) = + gateway_tasks_builder.try_build_upgrade_mode_watcher() + { + let req_sender = upgrade_mode_watcher.request_sender(); + upgrade_mode_watcher.start(); + req_sender + } else { + UpgradeModeCheckRequestSender::new_empty() + }; + + // create the common state for subtasks relying on the upgrade mode information + // (i.e. everything that'd require ticket/bandwidth processing) + let upgrade_mode_common_state = + gateway_tasks_builder.build_upgrade_mode_common_state(upgrade_check_request_sender); + // if we're running in entry mode, start the websocket if self.modes().entry { info!( @@ -634,7 +652,10 @@ impl NymNode { self.config.gateway_tasks.ws_bind_address ); let mut websocket = gateway_tasks_builder - .build_websocket_listener(active_clients_store.clone()) + .build_websocket_listener( + active_clients_store.clone(), + upgrade_mode_common_state.clone(), + ) .await?; self.shutdown_tracker() .try_spawn_named(async move { websocket.run().await }, "EntryWebsocket"); @@ -682,7 +703,7 @@ impl NymNode { gateway_tasks_builder.set_wireguard_data(wg_data.into()); let authenticator = gateway_tasks_builder - .build_wireguard_authenticator(topology_provider) + .build_wireguard_authenticator(upgrade_mode_common_state.clone(), topology_provider) .await?; let started_authenticator = authenticator.start_service_provider().await?; active_clients_store.insert_embedded(started_authenticator.handle); @@ -693,7 +714,7 @@ impl NymNode { ); gateway_tasks_builder - .try_start_wireguard() + .try_start_wireguard(upgrade_mode_common_state) .await .map_err(NymNodeError::GatewayTasksStartupFailure)?; } else { diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 26df1b52e6..120e1cc969 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -385,6 +385,18 @@ dependencies = [ "rayon", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-broadcast" version = "0.7.2" @@ -650,6 +662,12 @@ dependencies = [ "serde", ] +[[package]] +name = "binstring" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0669d5a35b64fdb5ab7fb19cae13148b6b5cbdf4b8247faf54ece47f699c8cef" + [[package]] name = "bip32" version = "0.5.3" @@ -721,6 +739,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "blake2b_simd" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -1069,6 +1098,17 @@ dependencies = [ "error-code", ] +[[package]] +name = "coarsetime" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91849686042de1b41cd81490edc83afbcb0abe5a9b6f2c4114f23ce8cca1bcf4" +dependencies = [ + "libc", + "wasix", + "wasm-bindgen", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -1127,6 +1167,12 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.4.0" @@ -1421,6 +1467,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "ct-codecs" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8" + [[package]] name = "ctor" version = "0.2.9" @@ -1624,6 +1676,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -1869,6 +1922,16 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-compact" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b3460f44bea8cd47f45a0c70892f1eff856d97cd55358b2f73f663789f6190" +dependencies = [ + "ct-codecs", + "getrandom 0.2.15", +] + [[package]] name = "ed25519-consensus" version = "2.1.0" @@ -1930,6 +1993,8 @@ dependencies = [ "ff", "generic-array", "group", + "hkdf", + "pem-rfc7468", "pkcs8", "rand_core 0.6.4", "sec1", @@ -2886,6 +2951,15 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -2895,6 +2969,30 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "hmac-sha1-compact" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18492c9f6f9a560e0d346369b665ad2bdbc89fa9bceca75796584e79042694c3" + +[[package]] +name = "hmac-sha256" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad6880c8d4a9ebf39c6e8b77007ce223f646a4d21ce29d99f70cb16420545425" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-sha512" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89e8d20b3799fa526152a5301a771eaaad80857f83e01b23216ceaafb2d9280" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "hostname" version = "0.4.0" @@ -3560,6 +3658,32 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jwt-simple" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "731011e9647a71ff4f8474176ff6ce6e0d2de87a0173f15613af3a84c3e3401a" +dependencies = [ + "anyhow", + "binstring", + "blake2b_simd", + "coarsetime", + "ct-codecs", + "ed25519-compact", + "hmac-sha1-compact", + "hmac-sha256", + "hmac-sha512", + "k256", + "p256", + "p384", + "rand 0.8.5", + "serde", + "serde_json", + "superboring", + "thiserror 2.0.12", + "zeroize", +] + [[package]] name = "k256" version = "0.13.4" @@ -3603,6 +3727,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libappindicator" @@ -3644,6 +3771,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "libredox" version = "0.1.3" @@ -3936,6 +4069,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -3951,6 +4101,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3958,6 +4119,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -4111,6 +4273,7 @@ dependencies = [ "nym-compact-ecash", "nym-ecash-time", "nym-network-defaults", + "nym-upgrade-mode-check", "rand 0.8.5", "serde", "strum", @@ -4127,6 +4290,7 @@ dependencies = [ "base64 0.22.1", "bs58", "ed25519-dalek", + "jwt-simple", "nym-pemstore", "nym-sphinx-types", "rand 0.8.5", @@ -4429,6 +4593,21 @@ dependencies = [ "x25519-dalek", ] +[[package]] +name = "nym-upgrade-mode-check" +version = "0.1.0" +dependencies = [ + "jwt-simple", + "nym-crypto", + "nym-http-api-client", + "reqwest 0.12.15", + "serde", + "serde_json", + "thiserror 2.0.12", + "time", + "tracing", +] + [[package]] name = "nym-validator-client" version = "0.1.0" @@ -4529,9 +4708,7 @@ name = "nym-wireguard-types" version = "0.1.0" dependencies = [ "base64 0.22.1", - "log", - "nym-config", - "nym-network-defaults", + "nym-crypto", "serde", "thiserror 2.0.12", "x25519-dalek", @@ -4900,6 +5077,18 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + [[package]] name = "pairing" version = "0.23.0" @@ -5024,6 +5213,15 @@ dependencies = [ "regex", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -5262,6 +5460,17 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -5944,6 +6153,27 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "sha2 0.10.9", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -6619,6 +6849,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spki" version = "0.7.3" @@ -6714,6 +6950,19 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" +[[package]] +name = "superboring" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "515cce34a781d7250b8a65706e0f2a5b99236ea605cb235d4baed6685820478f" +dependencies = [ + "getrandom 0.2.15", + "hmac-sha256", + "hmac-sha512", + "rand 0.8.5", + "rsa", +] + [[package]] name = "swift-rs" version = "1.0.7" @@ -8079,6 +8328,15 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasix" +version = "0.12.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d" +dependencies = [ + "wasi 0.11.0+wasi-snapshot-preview1", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" From e09066858c2dcd1c9e8b980f6906942802b80d8f Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:21:50 +0100 Subject: [PATCH 082/180] bump up version --- scripts/nym-node-setup/nym-node-cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index 179ab0e767..def82b1d17 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -__version__ = "1.1.0" +__version__ = "1.2.0" __default_branch__ = "develop" import os From ab6e08dd13175fda82fdd1b2c49b2ff2ffa24282 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:27:38 +0100 Subject: [PATCH 083/180] fix logic of landing-page lookup --- .../nym-node-setup/setup-nginx-proxy-wss.sh | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh index 4b0fae6bda..b72cf6ed24 100644 --- a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh +++ b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh @@ -48,17 +48,25 @@ rm -f "${HTTP_CONF}" || true rm -f "${HTTPS_CONF}" || true rm -f "${WSS_CONF}" || true -################################################################################ -# step 2: landing page -################################################################################ +############################################################################### +# step 2: create landing page +############################################################################### mkdir -p "${WEBROOT}" -if ! curl -fsSL \ - https://raw.githubusercontent.com/nymtech/nym/develop/scripts/nym-node-setup/landing-page.html \ - -o "${WEBROOT}/index.html"; then +# script directory where Python CLI stores fetched scripts +SCRIPT_DIR="$(dirname "${ENV_FILE:-./env.sh}")" +LOCAL_FETCHED_PAGE="${SCRIPT_DIR}/landing-page.html" - cat > "${WEBROOT}/index.html" <<'EOF' +if [[ -s "${LOCAL_FETCHED_PAGE}" ]]; then + cp "${LOCAL_FETCHED_PAGE}" "${WEBROOT}/index.html" +else + if curl -fsSL \ + https://raw.githubusercontent.com/nymtech/nym/develop/scripts/nym-node-setup/landing-page.html \ + -o "${WEBROOT}/index.html"; then + : + else + cat > "${WEBROOT}/index.html" < nym node @@ -69,11 +77,13 @@ if ! curl -fsSL \ EOF - + fi fi echo "landing page at ${WEBROOT}/index.html" +echo "landing page at ${WEBROOT}/index.html" + ################################################################################ # step 3: HTTP :80 config ################################################################################ From d126d8e5a0e69a0da013b65e5845054ff98da1f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 14 Nov 2025 13:34:36 +0000 Subject: [PATCH 084/180] feat: upgrade mode: VPN adjustments (#6189) * placeholder handling of wg registration with upgrade mode token * include upgrade mode credentials as part of credential storage * introduce helper for decoding JWT payload * expose methods for removing emergency credentials from the storage * don't allow duplicate emergency credentials with the same content * added authenticator ClientMessage for upgrade mode check * retrieve credentials with longest expiration first * post rebasing fixes * fixed gateway config * feat: allow specifying minimum node performance for client init * nym-node UM improvements * fixed upgrade mode bandwidth on initial authentication * fix: logs and thresholds * expose attestation information from nym-node http api * additional logs * post rebasing fixes * make @simonwicky happy by removing empty lines in emergency_credential table definition * chore: remove '_' prefix for internal counters within in-mem ecash storage * improved import of 'UpgradeModeState' within the nym-node * use explicit time dependency within credential-storage * re-order imports within the gateway-client * moved 'AvailableBandwidth' definition to the monorepo --- Cargo.lock | 10 +- .../src/client_message.rs | 29 ++++- common/authenticator-requests/src/models.rs | 6 + common/bandwidth-controller/Cargo.toml | 7 +- common/bandwidth-controller/src/error.rs | 3 + common/bandwidth-controller/src/lib.rs | 15 ++- common/bandwidth-controller/src/traits.rs | 17 +++ .../src/cli_helpers/client_init.rs | 10 +- .../gateway-client/src/socket_state.rs | 16 ++- common/credential-storage/Cargo.toml | 1 + .../20251030120000_upgrade_mode_token.sql | 17 +++ .../credential-storage/src/backends/memory.rs | 67 +++++++++- .../credential-storage/src/backends/sqlite.rs | 73 ++++++++++- .../src/ephemeral_storage.rs | 37 +++++- common/credential-storage/src/models.rs | 18 +++ .../src/persistent_storage/mod.rs | 33 +++++ common/credential-storage/src/storage.rs | 22 +++- common/credential-verification/src/lib.rs | 1 + .../src/upgrade_mode.rs | 11 +- common/upgrade-mode-check/src/error.rs | 3 + common/upgrade-mode-check/src/jwt.rs | 73 ++++++++++- common/upgrade-mode-check/src/lib.rs | 4 +- .../shared/src/lib.rs | 4 +- .../shared/src/models/interface.rs | 42 ++++++ .../websocket/connection_handler/fresh.rs | 50 +++++--- gateway/src/node/mod.rs | 6 +- gateway/src/node/upgrade_mode/watcher.rs | 34 ++++- nym-authenticator-client/src/error.rs | 6 + nym-authenticator-client/src/lib.rs | 120 ++++++++++++++---- .../src/attestation_watcher.rs | 31 ++++- nym-node/Cargo.toml | 1 + nym-node/nym-node-requests/Cargo.toml | 3 +- nym-node/nym-node-requests/src/api/v1/mod.rs | 1 + .../nym-node-requests/src/api/v1/network.rs | 4 + .../src/api/v1/network/models.rs | 28 ++++ nym-node/nym-node-requests/src/lib.rs | 13 ++ nym-node/src/cli/commands/run/mod.rs | 2 +- nym-node/src/cli/helpers.rs | 14 ++ nym-node/src/config/gateway_tasks.rs | 1 + nym-node/src/config/mod.rs | 4 + nym-node/src/env.rs | 2 + nym-node/src/node/http/router/api/v1/mod.rs | 2 + .../node/http/router/api/v1/network/mod.rs | 14 ++ .../router/api/v1/network/upgrade_mode.rs | 39 ++++++ .../src/node/http/router/api/v1/openapi.rs | 45 ++++--- nym-node/src/node/http/state/mod.rs | 17 ++- nym-node/src/node/mod.rs | 16 ++- nym-wallet/Cargo.lock | 4 +- tools/echo-server/Cargo.toml | 2 +- .../src/manager/local_client.rs | 14 +- .../src/manager/local_nodes.rs | 4 +- 51 files changed, 877 insertions(+), 119 deletions(-) create mode 100644 common/credential-storage/migrations/20251030120000_upgrade_mode_token.sql create mode 100644 nym-node/nym-node-requests/src/api/v1/network.rs create mode 100644 nym-node/nym-node-requests/src/api/v1/network/models.rs create mode 100644 nym-node/src/node/http/router/api/v1/network/mod.rs create mode 100644 nym-node/src/node/http/router/api/v1/network/upgrade_mode.rs diff --git a/Cargo.lock b/Cargo.lock index b9bcc7eb58..7ec3531bb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5005,21 +5005,16 @@ name = "nym-bandwidth-controller" version = "0.1.0" dependencies = [ "async-trait", - "bip39", "log", "nym-credential-storage", "nym-credentials", "nym-credentials-interface", "nym-crypto", - "nym-ecash-contract-common", "nym-ecash-time", - "nym-network-defaults", "nym-task", "nym-validator-client", "rand 0.8.5", "thiserror 2.0.12", - "url", - "zeroize", ] [[package]] @@ -5572,6 +5567,7 @@ dependencies = [ "sqlx", "sqlx-pool-guard", "thiserror 2.0.12", + "time", "tokio", "zeroize", ] @@ -6443,6 +6439,7 @@ dependencies = [ "nym-bin-common", "nym-client-core-config-types", "nym-config", + "nym-credential-verification", "nym-crypto", "nym-gateway", "nym-gateway-stats-storage", @@ -6515,13 +6512,13 @@ version = "0.1.0" dependencies = [ "async-trait", "celes", - "humantime", "humantime-serde", "nym-bin-common", "nym-crypto", "nym-exit-policy", "nym-http-api-client", "nym-noise-keys", + "nym-upgrade-mode-check", "nym-wireguard-types", "rand_chacha 0.3.1", "schemars 0.8.22", @@ -6532,6 +6529,7 @@ dependencies = [ "thiserror 2.0.12", "time", "tokio", + "url", "utoipa", ] diff --git a/common/authenticator-requests/src/client_message.rs b/common/authenticator-requests/src/client_message.rs index 23bdd13ab0..45f7481a46 100644 --- a/common/authenticator-requests/src/client_message.rs +++ b/common/authenticator-requests/src/client_message.rs @@ -6,7 +6,10 @@ use nym_wireguard_types::PeerPublicKey; use crate::{ AuthenticatorVersion, Error, - traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, Versionable}, + traits::{ + FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, UpgradeModeMessage, + Versionable, + }, v2, v3, v4, v5, v6, }; @@ -18,6 +21,7 @@ pub enum ClientMessage { Final(Box), Query(Box), TopUp(Box), + UpgradeModeCheck(Box), } pub struct SerialisedRequest { @@ -131,6 +135,7 @@ impl ClientMessage { ); Ok(SerialisedRequest::new(req.to_bytes()?, id)) } + _ => Err(Error::UnsupportedMessage), } } @@ -189,6 +194,7 @@ impl ClientMessage { ); Ok(SerialisedRequest::new(req.to_bytes()?, id)) } + _ => Err(Error::UnsupportedMessage), } } @@ -237,6 +243,7 @@ impl ClientMessage { }); Ok(SerialisedRequest::new(req.to_bytes()?, id)) } + _ => Err(Error::UnsupportedMessage), } } @@ -245,6 +252,7 @@ impl ClientMessage { registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair}, request::AuthenticatorRequest, topup::TopUpMessage, + upgrade_mode_check::UpgradeModeCheckRequest, }; match self { ClientMessage::Initial(init_message) => { @@ -282,6 +290,22 @@ impl ClientMessage { }); Ok(SerialisedRequest::new(req.to_bytes()?, id)) } + ClientMessage::UpgradeModeCheck(upgrade_mode_check) => { + // currently JWT is the only emergency credential option + let Some(upgrade_mode_jwt) = + upgrade_mode_check.upgrade_mode_global_attestation_jwt() + else { + return Err(Error::conversion( + "no valid known upgrade mode check variants", + )); + }; + let msg = UpgradeModeCheckRequest::UpgradeModeJwt { + token: upgrade_mode_jwt, + }; + + let (req, id) = AuthenticatorRequest::new_upgrade_mode_check_request(msg); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } } } } @@ -292,7 +316,7 @@ impl ClientMessage { match self { Self::Final(msg) => msg.credential().is_some(), Self::TopUp(_) => true, - Self::Initial(_) | Self::Query(_) => false, + Self::Initial(_) | Self::Query(_) | Self::UpgradeModeCheck(_) => false, } } @@ -302,6 +326,7 @@ impl ClientMessage { ClientMessage::Final(msg) => msg.version(), ClientMessage::Query(msg) => msg.version(), ClientMessage::TopUp(msg) => msg.version(), + ClientMessage::UpgradeModeCheck(msg) => msg.version(), } } diff --git a/common/authenticator-requests/src/models.rs b/common/authenticator-requests/src/models.rs index dc870df4d5..c01eec98f4 100644 --- a/common/authenticator-requests/src/models.rs +++ b/common/authenticator-requests/src/models.rs @@ -14,6 +14,12 @@ pub enum CurrentUpgradeModeStatus { Unknown, } +impl CurrentUpgradeModeStatus { + pub fn is_enabled(&self) -> bool { + matches!(self, CurrentUpgradeModeStatus::Enabled) + } +} + impl From for CurrentUpgradeModeStatus { fn from(value: bool) -> Self { if value { diff --git a/common/bandwidth-controller/Cargo.toml b/common/bandwidth-controller/Cargo.toml index 075c1702ae..a94abcc1cb 100644 --- a/common/bandwidth-controller/Cargo.toml +++ b/common/bandwidth-controller/Cargo.toml @@ -7,21 +7,16 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -async-trait = { workspace = true } -bip39 = { workspace = true } +async-trait = { workspace = true } log = { workspace = true } rand = { workspace = true } thiserror = { workspace = true } -url = { workspace = true } -zeroize = { workspace = true } nym-credential-storage = { path = "../credential-storage" } nym-credentials = { path = "../credentials" } nym-credentials-interface = { path = "../credentials-interface" } nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "stream_cipher", "aes", "hashing"] } -nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } nym-ecash-time = { path = "../ecash-time" } -nym-network-defaults = { path = "../network-defaults" } nym-task = { path = "../task" } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } diff --git a/common/bandwidth-controller/src/error.rs b/common/bandwidth-controller/src/error.rs index 4fcff3731a..c5721df092 100644 --- a/common/bandwidth-controller/src/error.rs +++ b/common/bandwidth-controller/src/error.rs @@ -21,6 +21,9 @@ pub enum BandwidthControllerError { #[error("There was a credential storage error - {0}")] CredentialStorageError(Box), + #[error("retrieved upgrade mode token is not a valid String")] + MalformedUpgradeModeToken, + #[error("the credential storage does not contain any usable credentials")] NoCredentialsAvailable, diff --git a/common/bandwidth-controller/src/lib.rs b/common/bandwidth-controller/src/lib.rs index 05be75cbdc..8002e77631 100644 --- a/common/bandwidth-controller/src/lib.rs +++ b/common/bandwidth-controller/src/lib.rs @@ -12,7 +12,7 @@ use crate::utils::{ ApiClientsWrapper, }; use log::error; -use nym_credential_storage::models::RetrievedTicketbook; +use nym_credential_storage::models::{EmergencyCredential, RetrievedTicketbook}; use nym_credential_storage::storage::Storage; use nym_credentials::ecash::bandwidth::CredentialSpendingData; use nym_credentials_interface::{ @@ -220,6 +220,19 @@ impl BandwidthController { } } } + + pub async fn get_emergency_credential( + &self, + typ: &str, + ) -> Result, BandwidthControllerError> + where + ::StorageError: Send + Sync + 'static, + { + self.storage + .get_emergency_credential(typ) + .await + .map_err(BandwidthControllerError::credential_storage_error) + } } impl Clone for BandwidthController diff --git a/common/bandwidth-controller/src/traits.rs b/common/bandwidth-controller/src/traits.rs index 8ff508f3f3..d611649004 100644 --- a/common/bandwidth-controller/src/traits.rs +++ b/common/bandwidth-controller/src/traits.rs @@ -11,6 +11,9 @@ use crate::{error::BandwidthControllerError, BandwidthController, PreparedCreden pub const DEFAULT_TICKETS_TO_SPEND: u32 = 1; +// TODO: this does not really belong here +pub const UPGRADE_MODE_JWT_TYPE: &str = "UPGRADE_MODE_JWT"; + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait BandwidthTicketProvider: Send + Sync { @@ -20,6 +23,8 @@ pub trait BandwidthTicketProvider: Send + Sync { gateway_id: ed25519::PublicKey, tickets_to_spend: u32, ) -> Result; + + async fn get_upgrade_mode_token(&self) -> Result, BandwidthControllerError>; } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -39,4 +44,16 @@ where self.prepare_ecash_ticket(ticket_type, gateway_id.to_bytes(), tickets_to_spend) .await } + + async fn get_upgrade_mode_token(&self) -> Result, BandwidthControllerError> { + let Some(emergency_credential) = + self.get_emergency_credential(UPGRADE_MODE_JWT_TYPE).await? + else { + return Ok(None); + }; + // upgrade mode credential is just a simple stringified JWT + let token = String::from_utf8(emergency_credential.data.content) + .map_err(|_| BandwidthControllerError::MalformedUpgradeModeToken)?; + Ok(Some(token)) + } } diff --git a/common/client-core/src/cli_helpers/client_init.rs b/common/client-core/src/cli_helpers/client_init.rs index feda3ab8d1..57f800c8cf 100644 --- a/common/client-core/src/cli_helpers/client_init.rs +++ b/common/client-core/src/cli_helpers/client_init.rs @@ -81,6 +81,10 @@ pub struct CommonClientInitArgs { #[cfg_attr(feature = "cli", clap(long, hide = true))] pub enabled_credentials_mode: Option, + /// Change the default minimum node performance used during initial node selection filtering. + #[cfg_attr(feature = "cli", clap(long, hide = true))] + pub minimum_gateway_performance: Option, + /// Mostly debug-related option to increase default traffic rate so that you would not need to /// modify config post init #[cfg_attr(feature = "cli", clap(long, hide = true))] @@ -173,10 +177,14 @@ where })?; hardcoded_topology.entry_capable_nodes().cloned().collect() } else { + let minimum_performance = common_args + .minimum_gateway_performance + .unwrap_or(core.debug.topology.minimum_gateway_performance); + crate::init::helpers::gateways_for_init( &core.client.nym_api_urls, user_agent, - core.debug.topology.minimum_gateway_performance, + minimum_performance, core.debug.topology.ignore_ingress_epoch_role, None, ) diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index 151ef4842e..af98d1b797 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::bandwidth::ClientBandwidth; +use crate::client::config::BandwidthTickets; use crate::error::GatewayClientError; use crate::packet_router::PacketRouter; use crate::traits::GatewayPacketRouter; @@ -177,7 +178,20 @@ impl PartiallyDelegatedRouter { let available_bi2 = bibytes2(available as f64); let required_bi2 = bibytes2(required as f64); warn!("run out of bandwidth when attempting to send the message! we got {available_bi2} available, but needed at least {required_bi2} to send the previous message"); - self.client_bandwidth.update_and_log(available, None); + // if we run out of bandwidth (and tried to send reasonable amount of data), + // the upgrade mode is implicitly disabled, as otherwise we would have been + // to proceed + let upgrade_mode = if available + < BandwidthTickets::DEFAULT_REMAINING_BANDWIDTH_THRESHOLD + { + Some(false) + } else { + // we were attempting to send a lot of data at once + // - we have no certainty about upgrade mode at this point + None + }; + self.client_bandwidth + .update_and_log(available, upgrade_mode); // UNIMPLEMENTED: we should stop sending messages until we recover bandwidth Ok(()) } diff --git a/common/credential-storage/Cargo.toml b/common/credential-storage/Cargo.toml index 1b0539dda4..9ca0eab6f3 100644 --- a/common/credential-storage/Cargo.toml +++ b/common/credential-storage/Cargo.toml @@ -14,6 +14,7 @@ bincode = { workspace = true, optional = true } log = { workspace = true } thiserror = { workspace = true } serde = { workspace = true, features = ["derive"], optional = true } +time = { workspace = true } tokio = { workspace = true, features = ["sync"] } zeroize = { workspace = true, features = ["zeroize_derive"] } diff --git a/common/credential-storage/migrations/20251030120000_upgrade_mode_token.sql b/common/credential-storage/migrations/20251030120000_upgrade_mode_token.sql new file mode 100644 index 0000000000..7f4c6f5ec8 --- /dev/null +++ b/common/credential-storage/migrations/20251030120000_upgrade_mode_token.sql @@ -0,0 +1,17 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +CREATE TABLE emergency_credential +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL, + -- don't define any strict schema on the content as it might be implementation-dependant + content BLOB NOT NULL, + expiration TIMESTAMP WITHOUT TIME ZONE +); + +-- no point in allowing duplicate data +CREATE UNIQUE INDEX emergency_credential_unique_type_content + ON emergency_credential (type, content); diff --git a/common/credential-storage/src/backends/memory.rs b/common/credential-storage/src/backends/memory.rs index 53207a4060..28b99e4196 100644 --- a/common/credential-storage/src/backends/memory.rs +++ b/common/credential-storage/src/backends/memory.rs @@ -1,7 +1,10 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; +use crate::models::{ + BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent, + RetrievedPendingTicketbook, RetrievedTicketbook, +}; use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; use nym_compact_ecash::VerificationKeyAuth; @@ -22,6 +25,12 @@ pub struct MemoryEcachTicketbookManager { inner: Arc>, } +#[derive(Default)] +struct InternalIdCounters { + next_ticketbook_id: i64, + next_emergency_credential_id: i64, +} + #[derive(Default)] struct EcashCredentialManagerInner { ticketbooks: HashMap, @@ -29,13 +38,22 @@ struct EcashCredentialManagerInner { master_vk: HashMap, coin_indices_sigs: HashMap>, expiration_date_sigs: HashMap<(u64, Date), Vec>, - _next_id: i64, + emergency_credentials: HashMap>, + + // internal counters emulating assignment of an increasing id to new inserted database entries + internal_counters: InternalIdCounters, } impl EcashCredentialManagerInner { - fn next_id(&mut self) -> i64 { - let next = self._next_id; - self._next_id += 1; + fn next_ticketbook_id(&mut self) -> i64 { + let next = self.internal_counters.next_ticketbook_id; + self.internal_counters.next_ticketbook_id += 1; + next + } + + fn next_emergency_credential_id(&mut self) -> i64 { + let next = self.internal_counters.next_emergency_credential_id; + self.internal_counters.next_emergency_credential_id += 1; next } } @@ -170,7 +188,7 @@ impl MemoryEcachTicketbookManager { used_tickets: u32, ) { let mut guard = self.inner.write().await; - let id = guard.next_id(); + let id = guard.next_ticketbook_id(); #[allow(clippy::unwrap_used)] let mut nasty_clone = hack_clone_ticketbook(ticketbook); @@ -277,4 +295,41 @@ impl MemoryEcachTicketbookManager { sigs.signatures.clone(), ); } + + pub(crate) async fn get_emergency_credential(&self, typ: &str) -> Option { + let guard = self.inner.read().await; + + guard.emergency_credentials.get(typ)?.first().cloned() + } + + pub(crate) async fn insert_emergency_credential( + &self, + credential: &EmergencyCredentialContent, + ) { + let mut guard = self.inner.write().await; + let id = guard.next_emergency_credential_id(); + + guard + .emergency_credentials + .entry(credential.typ.clone()) + .or_default() + .push(EmergencyCredential { + id, + data: credential.clone(), + }); + } + + pub(crate) async fn remove_emergency_credential(&self, id: i64) { + let mut guard = self.inner.write().await; + + guard.emergency_credentials.retain(|_, credentials| { + credentials.retain(|c| c.id != id); + !credentials.is_empty() + }) + } + + pub(crate) async fn remove_emergency_credentials_of_type(&self, typ: &str) { + let mut guard = self.inner.write().await; + guard.emergency_credentials.remove(typ); + } } diff --git a/common/credential-storage/src/backends/sqlite.rs b/common/credential-storage/src/backends/sqlite.rs index d196cad049..cb1cddf980 100644 --- a/common/credential-storage/src/backends/sqlite.rs +++ b/common/credential-storage/src/backends/sqlite.rs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::models::{ - BasicTicketbookInformation, RawCoinIndexSignatures, RawExpirationDateSignatures, - RawVerificationKey, StoredIssuedTicketbook, StoredPendingTicketbook, + BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent, + RawCoinIndexSignatures, RawExpirationDateSignatures, RawVerificationKey, + StoredIssuedTicketbook, StoredPendingTicketbook, }; use nym_ecash_time::Date; use sqlx::{Executor, Sqlite, Transaction}; @@ -305,6 +306,74 @@ impl SqliteEcashTicketbookManager { .await?; Ok(()) } + + pub(crate) async fn get_emergency_credential( + &self, + typ: &str, + ) -> Result, sqlx::Error> { + sqlx::query_as( + r#" + SELECT * + FROM emergency_credential + WHERE type = ? + AND (expiration IS NULL OR expiration > CURRENT_TIMESTAMP) + ORDER BY expiration DESC NULLS LAST + LIMIT 1 + "#, + ) + .bind(typ) + .fetch_optional(&*self.connection_pool) + .await + } + + pub(crate) async fn insert_emergency_credential( + &self, + credential: &EmergencyCredentialContent, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO emergency_credential + (type, content, expiration) + VALUES (?, ?, ?) + ON CONFLICT(type, content) DO NOTHING; + "#, + credential.typ, + credential.content, + credential.expiration, + ) + .execute(&*self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn remove_emergency_credential(&self, id: i64) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + DELETE FROM emergency_credential + WHERE id = ? + "#, + id + ) + .execute(&*self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn remove_emergency_credentials_of_type( + &self, + typ: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + DELETE FROM emergency_credential + WHERE type = ? + "#, + typ + ) + .execute(&*self.connection_pool) + .await?; + Ok(()) + } } pub(crate) async fn get_next_unspent_ticketbook<'a, E>( diff --git a/common/credential-storage/src/ephemeral_storage.rs b/common/credential-storage/src/ephemeral_storage.rs index 4c11a456e9..cd65bf3cca 100644 --- a/common/credential-storage/src/ephemeral_storage.rs +++ b/common/credential-storage/src/ephemeral_storage.rs @@ -3,7 +3,10 @@ use crate::backends::memory::MemoryEcachTicketbookManager; use crate::error::StorageError; -use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; +use crate::models::{ + BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent, + RetrievedPendingTicketbook, RetrievedTicketbook, +}; use crate::storage::Storage; use async_trait::async_trait; use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; @@ -218,6 +221,38 @@ impl Storage for EphemeralStorage { .await; Ok(()) } + + async fn get_emergency_credential( + &self, + typ: &str, + ) -> Result, Self::StorageError> { + Ok(self.storage_manager.get_emergency_credential(typ).await) + } + + async fn insert_emergency_credential( + &self, + credential: &EmergencyCredentialContent, + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_emergency_credential(credential) + .await; + Ok(()) + } + + async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError> { + self.storage_manager.remove_emergency_credential(id).await; + Ok(()) + } + + async fn remove_emergency_credentials_of_type( + &self, + typ: &str, + ) -> Result<(), Self::StorageError> { + self.storage_manager + .remove_emergency_credentials_of_type(typ) + .await; + Ok(()) + } } #[cfg(test)] diff --git a/common/credential-storage/src/models.rs b/common/credential-storage/src/models.rs index 5be1c45d29..f98bcd1f09 100644 --- a/common/credential-storage/src/models.rs +++ b/common/credential-storage/src/models.rs @@ -3,6 +3,7 @@ use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; use nym_ecash_time::Date; +use time::OffsetDateTime; use zeroize::{Zeroize, ZeroizeOnDrop}; pub struct RetrievedTicketbook { @@ -78,3 +79,20 @@ pub struct RawVerificationKey { pub serialised_key: Vec, pub serialization_revision: u8, } + +#[derive(Clone, Debug)] +#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] +pub struct EmergencyCredential { + pub id: i64, + #[cfg_attr(not(target_arch = "wasm32"), sqlx(flatten))] + pub data: EmergencyCredentialContent, +} + +#[derive(Clone, Debug)] +#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] +pub struct EmergencyCredentialContent { + #[cfg_attr(not(target_arch = "wasm32"), sqlx(rename = "type"))] + pub typ: String, + pub content: Vec, + pub expiration: Option, +} diff --git a/common/credential-storage/src/persistent_storage/mod.rs b/common/credential-storage/src/persistent_storage/mod.rs index b3302983ee..debef28093 100644 --- a/common/credential-storage/src/persistent_storage/mod.rs +++ b/common/credential-storage/src/persistent_storage/mod.rs @@ -3,6 +3,7 @@ mod legacy_helpers; +use crate::models::{EmergencyCredential, EmergencyCredentialContent}; use crate::{ backends::sqlite::{ get_next_unspent_ticketbook, increase_used_ticketbook_tickets, SqliteEcashTicketbookManager, @@ -401,4 +402,36 @@ impl Storage for PersistentStorage { .await?; Ok(()) } + + async fn get_emergency_credential( + &self, + typ: &str, + ) -> Result, Self::StorageError> { + Ok(self.storage_manager.get_emergency_credential(typ).await?) + } + + async fn insert_emergency_credential( + &self, + credential: &EmergencyCredentialContent, + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_emergency_credential(credential) + .await?; + Ok(()) + } + + async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError> { + self.storage_manager.remove_emergency_credential(id).await?; + Ok(()) + } + + async fn remove_emergency_credentials_of_type( + &self, + typ: &str, + ) -> Result<(), Self::StorageError> { + self.storage_manager + .remove_emergency_credentials_of_type(typ) + .await?; + Ok(()) + } } diff --git a/common/credential-storage/src/storage.rs b/common/credential-storage/src/storage.rs index add8131e12..483a4571c5 100644 --- a/common/credential-storage/src/storage.rs +++ b/common/credential-storage/src/storage.rs @@ -1,7 +1,10 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; +use crate::models::{ + BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent, + RetrievedPendingTicketbook, RetrievedTicketbook, +}; use async_trait::async_trait; use nym_compact_ecash::VerificationKeyAuth; use nym_credentials::ecash::bandwidth::serialiser::keys::EpochVerificationKey; @@ -108,4 +111,21 @@ pub trait Storage: Clone + Send + Sync { &self, signatures: &AggregatedExpirationDateSignatures, ) -> Result<(), Self::StorageError>; + + async fn get_emergency_credential( + &self, + typ: &str, + ) -> Result, Self::StorageError>; + + async fn insert_emergency_credential( + &self, + credential: &EmergencyCredentialContent, + ) -> Result<(), Self::StorageError>; + + async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError>; + + async fn remove_emergency_credentials_of_type( + &self, + typ: &str, + ) -> Result<(), Self::StorageError>; } diff --git a/common/credential-verification/src/lib.rs b/common/credential-verification/src/lib.rs index 3f2b77f840..430674c991 100644 --- a/common/credential-verification/src/lib.rs +++ b/common/credential-verification/src/lib.rs @@ -13,6 +13,7 @@ use tracing::*; pub use client_bandwidth::*; pub use error::*; +pub use upgrade_mode::UpgradeModeState; pub mod bandwidth_storage_manager; mod client_bandwidth; diff --git a/common/credential-verification/src/upgrade_mode.rs b/common/credential-verification/src/upgrade_mode.rs index 81385bece1..7dec1010f7 100644 --- a/common/credential-verification/src/upgrade_mode.rs +++ b/common/credential-verification/src/upgrade_mode.rs @@ -12,7 +12,7 @@ use std::time::Duration; use thiserror::Error; use time::OffsetDateTime; use tokio::sync::{Notify, RwLock}; -use tracing::{debug, error}; +use tracing::{debug, error, info}; #[derive(Debug, Error)] pub enum UpgradeModeEnableError { @@ -101,6 +101,10 @@ impl UpgradeModeDetails { } } + pub fn state(&self) -> &UpgradeModeState { + &self.state + } + pub fn enabled(&self) -> bool { self.state.upgrade_mode_enabled() } @@ -156,6 +160,7 @@ impl UpgradeModeDetails { // note: if attestation has been returned, it means we're definitely in upgrade mode // (otherwise it wouldn't have existed in the state) + info!("managed to initialise upgrade mode through received JWT"); Ok(()) } @@ -198,6 +203,10 @@ impl UpgradeModeState { } } + pub fn attester_pubkey(&self) -> ed25519::PublicKey { + self.inner.expected_attester_public_key + } + pub async fn attestation(&self) -> Option { self.inner.expected_attestation.read().await.clone() } diff --git a/common/upgrade-mode-check/src/error.rs b/common/upgrade-mode-check/src/error.rs index 3b2f2b5b12..acb9ec198c 100644 --- a/common/upgrade-mode-check/src/error.rs +++ b/common/upgrade-mode-check/src/error.rs @@ -9,6 +9,9 @@ pub enum UpgradeModeCheckError { #[error("failed to decode jwt metadata: {source}")] TokenMetadataDecodeFailure { source: jwt_simple::Error }, + #[error("the upgrade mode JWT is malformed")] + MalformedToken, + #[error("the jwt metadata didn't contain explicit public key")] MissingTokenPublicKey, diff --git a/common/upgrade-mode-check/src/jwt.rs b/common/upgrade-mode-check/src/jwt.rs index 64c436b23e..42329f80a5 100644 --- a/common/upgrade-mode-check/src/jwt.rs +++ b/common/upgrade-mode-check/src/jwt.rs @@ -4,7 +4,10 @@ use crate::{UpgradeModeAttestation, UpgradeModeCheckError}; use jwt_simple::claims::Claims; use jwt_simple::common::{KeyMetadata, VerificationOptions}; -use jwt_simple::prelude::{EdDSAKeyPairLike, EdDSAPublicKeyLike}; +use jwt_simple::prelude::{ + Base64UrlSafeNoPadding, EdDSAKeyPairLike, EdDSAPublicKeyLike, JWTClaims, +}; +use jwt_simple::reexports::ct_codecs::Decoder; use jwt_simple::token::Token; use nym_crypto::asymmetric::ed25519; use std::collections::HashSet; @@ -76,12 +79,26 @@ pub fn validate_upgrade_mode_jwt( Ok(attestation) } +/// Attempt to extract the upgrade mode JWT payload from the provided token +pub fn try_decode_upgrade_mode_jwt_claims( + token: &str, +) -> Result, UpgradeModeCheckError> { + let mut parts = token.split('.'); + let _header = parts.next().ok_or(UpgradeModeCheckError::MalformedToken)?; + let claims_b64 = parts.next().ok_or(UpgradeModeCheckError::MalformedToken)?; + let claims_bytes = Base64UrlSafeNoPadding::decode_to_vec(claims_b64, None) + .map_err(|_| UpgradeModeCheckError::MalformedToken)?; + + serde_json::from_slice(&claims_bytes).map_err(|_| UpgradeModeCheckError::MalformedToken) +} + #[cfg(test)] mod tests { use super::*; use crate::generate_new_attestation; use nym_crypto::asymmetric::ed25519; use nym_test_utils::helpers::deterministic_rng; + use time::OffsetDateTime; #[test] fn generate_and_validate_jwt() { @@ -142,4 +159,58 @@ mod tests { // we don't care about issuer assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, None).is_ok()); } + + #[test] + fn decode_upgrade_mode_claims() { + let invalid_jwts = [ + "", + "invalidSections", + "also.invalid.sections", + "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImp3ayI6IkZCdWsxS2lqS3ZwQ3VrU1Zhc0xoN1k1REZTZEdnVzU5WThQOUhWTDh2Mzk5In0.eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImp3ayI6IkZCdWsxS2lqS3ZwQ3VrU1Zhc0xoN1k1REZTZEdnVzU5WThQOUhWTDh2Mzk5In0.eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImp3ayI6IkZCdWsxS2lqS3ZwQ3VrU1Zhc0xoN1k1REZTZEdnVzU5WThQOUhWTDh2Mzk5In0", + ]; + + let attestation_key = ed25519::PrivateKey::from_bytes(&[ + 108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248, + 163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161, + ]) + .unwrap(); + let jwt_key = ed25519::PrivateKey::from_bytes(&[ + 152, 17, 144, 255, 213, 219, 246, 208, 109, 33, 100, 73, 1, 141, 32, 63, 141, 89, 167, + 2, 52, 215, 241, 219, 200, 18, 159, 241, 76, 111, 42, 32, + ]) + .unwrap(); + let jwt_keys = ed25519::KeyPair::from(jwt_key); + + let validity = Duration::from_secs(60 * 60); + let attestation = generate_new_attestation(&attestation_key, vec![*jwt_keys.public_key()]); + let valid_jwt = generate_jwt_for_upgrade_mode_attestation( + attestation.clone(), + validity, + &jwt_keys, + Some("nym-credential-proxy"), + ); + + for invalid in invalid_jwts { + assert!(try_decode_upgrade_mode_jwt_claims(invalid).is_err()) + } + + let decoded = try_decode_upgrade_mode_jwt_claims(&valid_jwt).unwrap(); + assert_eq!(decoded.issuer.unwrap(), "nym-credential-proxy"); + assert_eq!(decoded.custom, attestation); + + // unfortunately we can't inject current time when constructing the JWT so the best we can do is ensure its within error margin + let margin = Duration::from_secs(10); + let now = OffsetDateTime::now_utc(); + let min = now - margin; + let max = now + margin; + let issued = decoded.issued_at.unwrap(); + let issued_time = OffsetDateTime::from_unix_timestamp(issued.as_secs() as i64).unwrap(); + assert!(issued_time >= min && issued_time <= max); + + let min = now - margin + validity; + let max = now + margin + validity; + let expires = decoded.expires_at.unwrap(); + let expires_time = OffsetDateTime::from_unix_timestamp(expires.as_secs() as i64).unwrap(); + assert!(expires_time >= min && expires_time <= max); + } } diff --git a/common/upgrade-mode-check/src/lib.rs b/common/upgrade-mode-check/src/lib.rs index c2ab50284b..7a2e246e2d 100644 --- a/common/upgrade-mode-check/src/lib.rs +++ b/common/upgrade-mode-check/src/lib.rs @@ -11,8 +11,10 @@ pub use attestation::{ pub use error::UpgradeModeCheckError; pub use jwt::{ CREDENTIAL_PROXY_JWT_ISSUER, generate_jwt_for_upgrade_mode_attestation, - validate_upgrade_mode_jwt, + try_decode_upgrade_mode_jwt_claims, validate_upgrade_mode_jwt, }; #[cfg(not(target_arch = "wasm32"))] pub use attestation::attempt_retrieve_attestation; + +pub const UPGRADE_MODE_CREDENTIAL_TYPE: &str = "upgrade_mode_jwt"; diff --git a/common/wireguard-private-metadata/shared/src/lib.rs b/common/wireguard-private-metadata/shared/src/lib.rs index c0d711ac42..6a8eadcc24 100644 --- a/common/wireguard-private-metadata/shared/src/lib.rs +++ b/common/wireguard-private-metadata/shared/src/lib.rs @@ -10,7 +10,9 @@ pub mod routes; pub use models::v0; pub use models::{ AxumErrorResponse, AxumResult, Construct, ErrorResponse, Extract, Request, Response, Version, - error::Error as ModelError, interface, latest, v1, v2, + error::Error as ModelError, + interface::{self, AvailableBandwidth}, + latest, v1, v2, }; fn make_bincode_serializer() -> impl bincode::Options { diff --git a/common/wireguard-private-metadata/shared/src/models/interface.rs b/common/wireguard-private-metadata/shared/src/models/interface.rs index c97db77d2a..317e6680ca 100644 --- a/common/wireguard-private-metadata/shared/src/models/interface.rs +++ b/common/wireguard-private-metadata/shared/src/models/interface.rs @@ -229,3 +229,45 @@ impl Extract for Response { } } } + +#[derive(Debug, Clone, Copy)] +pub struct AvailableBandwidth { + pub bandwidth_bytes: i64, + pub upgrade_mode: Option, +} + +impl From for AvailableBandwidth { + fn from(value: v1::AvailableBandwidthResponse) -> Self { + AvailableBandwidth { + bandwidth_bytes: value.available_bandwidth, + upgrade_mode: None, + } + } +} + +impl From for AvailableBandwidth { + fn from(value: v2::AvailableBandwidthResponse) -> Self { + AvailableBandwidth { + bandwidth_bytes: value.available_bandwidth, + upgrade_mode: Some(value.upgrade_mode), + } + } +} + +impl From for AvailableBandwidth { + fn from(value: v1::TopUpResponse) -> Self { + AvailableBandwidth { + bandwidth_bytes: value.available_bandwidth, + upgrade_mode: None, + } + } +} + +impl From for AvailableBandwidth { + fn from(value: v2::TopUpResponse) -> Self { + AvailableBandwidth { + bandwidth_bytes: value.available_bandwidth, + upgrade_mode: Some(value.upgrade_mode), + } + } +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 0eb18a602a..ead0fb18f3 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -13,7 +13,7 @@ use futures::{ channel::{mpsc, oneshot}, SinkExt, StreamExt, }; -use nym_credentials_interface::AvailableBandwidth; +use nym_credentials_interface::{AvailableBandwidth, DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD}; use nym_crypto::aes::cipher::crypto_common::rand_core::RngCore; use nym_crypto::asymmetric::ed25519; use nym_gateway_requests::authenticate::AuthenticateRequest; @@ -35,6 +35,7 @@ use nym_node_metrics::events::MetricsEvent; use nym_sphinx::DestinationAddressBytes; use nym_task::ShutdownToken; use rand::CryptoRng; +use std::cmp::max; use std::net::SocketAddr; use std::time::Duration; use thiserror::Error; @@ -528,6 +529,35 @@ impl FreshHandler { } } + /// Determine the amount of remaining bandwidth the authenticated client should see. + /// This depends on whether the bandwidth stored persistently had already expired (in which case it's set back to 0) + /// and whether the upgrade mode is enabled. In that case the minimum constant amount is returned. + async fn authenticated_bandwidth_bytes( + &self, + client_id: i64, + ) -> Result { + // 1. get the actual registered bandwidth + let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?; + + // 2. check if it had already expired + let true_remaining_bandwidth = if available_bandwidth.expired() { + self.shared_state.storage.reset_bandwidth(client_id).await?; + 0 + } else { + available_bandwidth.bytes + }; + + // 3. perform upgrade mode adjustments + if self.upgrade_mode_enabled() { + Ok(max( + true_remaining_bandwidth, + DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD + 1, + )) + } else { + Ok(true_remaining_bandwidth) + } + } + /// Tries to handle the received authentication request by checking correctness of the received data. /// /// # Arguments @@ -594,14 +624,7 @@ impl FreshHandler { .await?; // check the bandwidth - let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?; - - let bandwidth_remaining = if available_bandwidth.expired() { - self.shared_state.storage.reset_bandwidth(client_id).await?; - 0 - } else { - available_bandwidth.bytes - }; + let bandwidth_remaining = self.authenticated_bandwidth_bytes(client_id).await?; Ok(InitialAuthResult::new( Some(ClientDetails::new( @@ -681,14 +704,7 @@ impl FreshHandler { .await?; // finally check and retrieve client's bandwidth - let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?; - - let bandwidth_remaining = if available_bandwidth.expired() { - self.shared_state.storage.reset_bandwidth(client_id).await?; - 0 - } else { - available_bandwidth.bytes - }; + let bandwidth_remaining = self.authenticated_bandwidth_bytes(client_id).await?; Ok(InitialAuthResult::new( Some(ClientDetails::new( diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 7f08a5e52c..ba891bd716 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -49,7 +49,7 @@ pub use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgen pub(crate) mod client_handling; pub(crate) mod internal_service_providers; mod stale_data_cleaner; -pub(crate) mod upgrade_mode; +pub mod upgrade_mode; #[derive(Debug, Clone)] pub struct LocalNetworkRequesterOpts { @@ -122,7 +122,7 @@ impl GatewayTasksBuilder { metrics: NymNodeMetrics, mnemonic: Arc>, user_agent: UserAgent, - upgrade_mode_attester_public_key: ed25519::PublicKey, + upgrade_mode_state: UpgradeModeState, shutdown_tracker: ShutdownTracker, ) -> GatewayTasksBuilder { GatewayTasksBuilder { @@ -137,7 +137,7 @@ impl GatewayTasksBuilder { mix_packet_sender, metrics_sender, metrics, - upgrade_mode_state: UpgradeModeState::new(upgrade_mode_attester_public_key), + upgrade_mode_state, mnemonic, shutdown_tracker, ecash_manager: None, diff --git a/gateway/src/node/upgrade_mode/watcher.rs b/gateway/src/node/upgrade_mode/watcher.rs index c798ddc55b..509e89c5f5 100644 --- a/gateway/src/node/upgrade_mode/watcher.rs +++ b/gateway/src/node/upgrade_mode/watcher.rs @@ -10,11 +10,17 @@ use nym_credential_verification::upgrade_mode::{ use nym_task::ShutdownToken; use nym_upgrade_mode_check::attempt_retrieve_attestation; use std::time::Duration; +use time::OffsetDateTime; use tokio::task::JoinHandle; use tokio::time::Instant; use tracing::{debug, error, info, trace}; use url::Url; +/// Specifies the threshold for retrieval failures that will trigger disabling upgrade mode. +/// This assumes the file has been removed incorrectly and has been replaced by some placeholder 404 +/// page that does not deserialise correctly +const FAILURE_THRESHOLD: usize = 5; + pub struct UpgradeModeWatcher { // default polling interval regular_polling_interval: Duration, @@ -35,6 +41,8 @@ pub struct UpgradeModeWatcher { user_agent: UserAgent, shutdown_token: ShutdownToken, + + consecutive_retrieval_failures: usize, } impl UpgradeModeWatcher { @@ -58,6 +66,7 @@ impl UpgradeModeWatcher { upgrade_mode_state, user_agent, shutdown_token, + consecutive_retrieval_failures: 0, } } @@ -65,7 +74,7 @@ impl UpgradeModeWatcher { self.check_request_sender.clone() } - async fn try_update_state(&self) { + async fn try_update_state(&mut self) { match attempt_retrieve_attestation( self.attestation_url.as_str(), Some(self.user_agent.clone()), @@ -73,10 +82,28 @@ impl UpgradeModeWatcher { .await { Err(err) => { + self.consecutive_retrieval_failures += 1; info!("upgrade mode attestation is not available at this time"); - debug!("retrieval error: {err}") + debug!("retrieval error: {err}"); + + if self.upgrade_mode_state.upgrade_mode_enabled() + && self.consecutive_retrieval_failures > FAILURE_THRESHOLD + { + self.upgrade_mode_state + .try_set_expected_attestation(None) + .await + } else { + self.upgrade_mode_state + .update_last_queried(OffsetDateTime::now_utc()); + } } Ok(attestation) => { + self.consecutive_retrieval_failures = 0; + if attestation.is_some() { + info!("retrieved valid attestation: attempting to begin upgrade mode") + } else { + info!("attempting to disable upgrade mode") + } self.upgrade_mode_state .try_set_expected_attestation(attestation) .await @@ -110,7 +137,8 @@ impl UpgradeModeWatcher { async fn run(&mut self) { info!("starting the update mode watcher"); - let check_wait = tokio::time::sleep(self.regular_polling_interval); + // make sure the first check happens immediately + let check_wait = tokio::time::sleep(Duration::new(0, 0)); tokio::pin!(check_wait); loop { diff --git a/nym-authenticator-client/src/error.rs b/nym-authenticator-client/src/error.rs index 142fbe6981..abf18e47ca 100644 --- a/nym-authenticator-client/src/error.rs +++ b/nym-authenticator-client/src/error.rs @@ -40,6 +40,12 @@ pub enum AuthenticationClientError { source: nym_bandwidth_controller::error::BandwidthControllerError, }, + #[error("failed to retrieve upgrade mode token")] + UpgradeModeToken { + #[source] + source: nym_bandwidth_controller::error::BandwidthControllerError, + }, + #[error("unknown authenticator version number")] UnsupportedAuthenticatorVersion, diff --git a/nym-authenticator-client/src/lib.rs b/nym-authenticator-client/src/lib.rs index 10b651b184..c09a766696 100644 --- a/nym-authenticator-client/src/lib.rs +++ b/nym-authenticator-client/src/lib.rs @@ -8,17 +8,18 @@ use nym_registration_common::GatewayData; use std::net::{IpAddr, SocketAddr}; use std::sync::Arc; use std::time::Duration; -use tracing::{debug, error, trace}; +use tracing::{debug, error, trace, warn}; use crate::error::Result; use crate::mixnet_listener::{MixnetMessageBroadcastReceiver, MixnetMessageInputSender}; use crate::types::{AvailableBandwidthClientResponse, TopUpClientResponse}; +use nym_authenticator_requests::models::BandwidthClaim; use nym_authenticator_requests::traits::UpgradeModeStatus; use nym_authenticator_requests::{ AuthenticatorVersion, client_message::ClientMessage, response::AuthenticatorResponse, traits::Id, v2, v3, v4, v5, v6, }; -use nym_credentials_interface::{CredentialSpendingData, TicketType}; +use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData, TicketType}; use nym_sdk::mixnet::{IncludedSurbs, Recipient, ReconstructedMessage}; use nym_service_provider_requests_common::{Protocol, ServiceProviderTypeExt}; use nym_wireguard_types::PeerPublicKey; @@ -205,6 +206,57 @@ impl AuthenticatorClient { } } + async fn produce_bandwidth_claim( + &self, + controller: &dyn BandwidthTicketProvider, + upgrade_mode_enabled: bool, + ticketbook_type: TicketType, + ) -> Result { + if upgrade_mode_enabled { + match controller + .get_upgrade_mode_token() + .await + .map_err(|source| AuthenticationClientError::UpgradeModeToken { source })? + { + None => warn!( + "the wireguard node is in the upgrade mode, whilst we do not have an upgrade mode token - we will have to use normal ZK nym instead" + ), + + Some(upgrade_mode_token) => { + return Ok(BandwidthClaim { + credential: BandwidthCredential::UpgradeModeJWT { + token: upgrade_mode_token, + }, + kind: ticketbook_type, + }); + } + } + } + + let credential = controller + .get_ecash_ticket( + ticketbook_type, + self.auth_recipient.gateway(), + DEFAULT_TICKETS_TO_SPEND, + ) + .await + .map_err(|source| AuthenticationClientError::GetTicket { + ticketbook_type, + source, + })? + .data; + + let credential = credential + .try_into() + .inspect_err(|err| { + error!( + "failed to convert {ticketbook_type} ticket to a valid BandwidthClaim: {err}" + ) + }) + .map_err(|_| AuthenticationClientError::InternalError)?; + Ok(credential) + } + pub async fn register_wireguard( &mut self, controller: &dyn BandwidthTicketProvider, @@ -255,33 +307,19 @@ impl AuthenticatorClient { &self.ip_addr, &pending_registration_response ); - // This call takes care of updating the credential count in storage, so failure of this must be counted as credential waste - let credential = Some( - controller - .get_ecash_ticket( - ticketbook_type, - self.auth_recipient.gateway(), - DEFAULT_TICKETS_TO_SPEND, - ) - .await - .map_err(|source| RegistrationError::CredentialSent { - source: AuthenticationClientError::GetTicket { - ticketbook_type, - source, - }, - })? - .data, - ); - let credential = credential - .map(TryInto::try_into) - .transpose() - .inspect_err(|err| error!("failed to convert {ticketbook_type} ticket to a valid BandwidthClaim: {err}")) - .map_err(|_| RegistrationError::CredentialSent { - source: AuthenticationClientError::InternalError, - })?; + // if the node reports upgrade mode, we can use the corresponding token for registration + // instead of spending zk-nym ticket + let upgrade_mode_enabled = pending_registration_response + .upgrade_mode_status() + .is_enabled(); + + let bandwidth_claim = self + .produce_bandwidth_claim(controller, upgrade_mode_enabled, ticketbook_type) + .await + .map_err(|source| RegistrationError::CredentialSent { source })?; let finalized_message = pending_registration_response - .finalise_registration(self.keypair.private_key(), credential); + .finalise_registration(self.keypair.private_key(), Some(bandwidth_claim)); let client_message = ClientMessage::Final(finalized_message); trace!("sending final msg to {}: {client_message:?}", &self.ip_addr); @@ -421,4 +459,32 @@ impl AuthenticatorClient { current_upgrade_mode_status, }) } + + pub async fn check_upgrade_mode(&mut self, upgrade_mode_jwt: String) -> Result { + let check_um_message = match self.auth_version { + AuthenticatorVersion::V1 + | AuthenticatorVersion::V2 + | AuthenticatorVersion::V3 + | AuthenticatorVersion::V4 + | AuthenticatorVersion::V5 + | AuthenticatorVersion::UNKNOWN => { + return Err(AuthenticationClientError::UnsupportedAuthenticatorVersion); + } + + AuthenticatorVersion::V6 => ClientMessage::UpgradeModeCheck(Box::new( + v6::upgrade_mode_check::UpgradeModeCheckRequest::UpgradeModeJwt { + token: upgrade_mode_jwt, + }, + )), + }; + + let response = self.send_and_wait_for_response(&check_um_message).await?; + let AuthenticatorResponse::UpgradeMode(upgrade_mode_check_response) = response else { + return Err(AuthenticationClientError::InvalidGatewayAuthResponse); + }; + + Ok(upgrade_mode_check_response + .upgrade_mode_status() + .is_enabled()) + } } diff --git a/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs b/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs index b544f80792..06931852fc 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs @@ -12,6 +12,11 @@ use tokio_util::sync::CancellationToken; use tracing::{debug, info}; use url::Url; +/// Specifies the threshold for retrieval failures that will trigger disabling upgrade mode. +/// This assumes the file has been removed incorrectly and has been replaced by some placeholder 404 +/// page that does not deserialise correctly +const FAILURE_THRESHOLD: usize = 5; + pub struct AttestationWatcher { // default polling interval regular_polling_interval: Duration, @@ -28,6 +33,8 @@ pub struct AttestationWatcher { jwt_validity: Duration, upgrade_mode_state: UpgradeModeState, + + consecutive_retrieval_failures: usize, } impl AttestationWatcher { @@ -49,6 +56,7 @@ impl AttestationWatcher { upgrade_mode_state: UpgradeModeState { inner: Arc::new(Default::default()), }, + consecutive_retrieval_failures: 0, } } @@ -56,7 +64,7 @@ impl AttestationWatcher { self.upgrade_mode_state.clone() } - async fn try_update_state(&self) { + async fn try_update_state(&mut self) { match attempt_retrieve_attestation( self.attestation_url.as_str(), Some(generate_user_agent!()), @@ -64,10 +72,26 @@ impl AttestationWatcher { .await { Err(err) => { + self.consecutive_retrieval_failures += 1; info!("upgrade mode attestation is not available at this time"); - debug!("retrieval error: {err}") + debug!("retrieval error: {err}"); + + if self.upgrade_mode_state.has_attestation().await + && self.consecutive_retrieval_failures > FAILURE_THRESHOLD + { + self.upgrade_mode_state + .update( + None, + self.expected_attester_public_key, + &self.jwt_signing_keys, + self.jwt_validity, + ) + .await + } } Ok(attestation) => { + self.consecutive_retrieval_failures = 0; + self.upgrade_mode_state .update( attestation, @@ -80,9 +104,10 @@ impl AttestationWatcher { } } - pub async fn run_forever(self, cancellation_token: CancellationToken) { + pub async fn run_forever(mut self, cancellation_token: CancellationToken) { info!("starting the attestation watcher task"); + // make sure the first check happens immediately let check_wait = tokio::time::sleep(Duration::new(0, 0)); tokio::pin!(check_wait); diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 6d43b0aa97..f431886ebf 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -57,6 +57,7 @@ nym-client-core-config-types = { path = "../common/client-core/config-types", fe "disk-persistence", ] } nym-config = { path = "../common/config" } +nym-credential-verification = { path = "../common/credential-verification" } nym-crypto = { path = "../common/crypto", features = ["asymmetric", "rand"] } nym-nonexhaustive-delayqueue = { path = "../common/nonexhaustive-delayqueue" } nym-mixnet-client = { path = "../common/client-libs/mixnet-client" } diff --git a/nym-node/nym-node-requests/Cargo.toml b/nym-node/nym-node-requests/Cargo.toml index 82119f47c8..ad1ed3013a 100644 --- a/nym-node/nym-node-requests/Cargo.toml +++ b/nym-node/nym-node-requests/Cargo.toml @@ -12,7 +12,6 @@ license.workspace = true [dependencies] celes = { workspace = true } # country codes -humantime = { workspace = true } humantime-serde = { workspace = true } schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } @@ -21,6 +20,7 @@ strum = { workspace = true, features = ["derive"] } strum_macros = { workspace = true } time = { workspace = true, features = ["serde", "formatting", "parsing"] } thiserror = { workspace = true } +url = { workspace = true, features = ["serde"] } nym-crypto = { path = "../../common/crypto", features = [ "asymmetric", @@ -29,6 +29,7 @@ nym-crypto = { path = "../../common/crypto", features = [ nym-exit-policy = { path = "../../common/exit-policy" } nym-noise-keys = { path = "../../common/nymnoise/keys" } nym-wireguard-types = { path = "../../common/wireguard-types", default-features = false } +nym-upgrade-mode-check = { path = "../../common/upgrade-mode-check", features = ["openapi"] } # feature-specific dependencies: diff --git a/nym-node/nym-node-requests/src/api/v1/mod.rs b/nym-node/nym-node-requests/src/api/v1/mod.rs index 544633944f..e19b611f1b 100644 --- a/nym-node/nym-node-requests/src/api/v1/mod.rs +++ b/nym-node/nym-node-requests/src/api/v1/mod.rs @@ -7,6 +7,7 @@ pub mod health; pub mod ip_packet_router; pub mod metrics; pub mod mixnode; +pub mod network; pub mod network_requester; pub mod node; pub mod node_load; diff --git a/nym-node/nym-node-requests/src/api/v1/network.rs b/nym-node/nym-node-requests/src/api/v1/network.rs new file mode 100644 index 0000000000..13811deb67 --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/network.rs @@ -0,0 +1,4 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod models; diff --git a/nym-node/nym-node-requests/src/api/v1/network/models.rs b/nym-node/nym-node-requests/src/api/v1/network/models.rs new file mode 100644 index 0000000000..be5d01b198 --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/network/models.rs @@ -0,0 +1,28 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_crypto::asymmetric::ed25519; +use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey; +use nym_upgrade_mode_check::UpgradeModeAttestation; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use url::Url; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct UpgradeModeStatus { + pub enabled: bool, + + #[serde(with = "time::serde::rfc3339")] + #[cfg_attr(feature = "openapi", schema(value_type = String))] + pub last_queried: OffsetDateTime, + + #[cfg_attr(feature = "openapi", schema(value_type = String))] + pub attestation_provider: Url, + + #[serde(with = "bs58_ed25519_pubkey")] + #[cfg_attr(feature = "openapi", schema(value_type = String))] + pub attester_pubkey: ed25519::PublicKey, + + pub published_attestation: Option, +} diff --git a/nym-node/nym-node-requests/src/lib.rs b/nym-node/nym-node-requests/src/lib.rs index 2b539f831e..aaf63baa25 100644 --- a/nym-node/nym-node-requests/src/lib.rs +++ b/nym-node/nym-node-requests/src/lib.rs @@ -36,6 +36,7 @@ pub mod routes { pub const AUXILIARY: &str = "/auxiliary-details"; pub const HEALTH: &str = "/health"; pub const LOAD: &str = "/load"; + pub const NETWORK: &str = "/network"; pub const SWAGGER: &str = "/swagger"; pub const GATEWAY: &str = "/gateway"; @@ -49,6 +50,7 @@ pub mod routes { // define helper functions to get absolute routes absolute_route!(health_absolute, v1_absolute(), HEALTH); absolute_route!(load_absolute, v1_absolute(), LOAD); + absolute_route!(network_absolute, v1_absolute(), NETWORK); absolute_route!(roles_absolute, v1_absolute(), ROLES); absolute_route!(build_info_absolute, v1_absolute(), BUILD_INFO); absolute_route!(host_info_absolute, v1_absolute(), HOST_INFO); @@ -131,6 +133,17 @@ pub mod routes { pub mod ip_packet_router { // use super::*; } + + pub mod network { + use super::*; + pub const UPGRADE_MODE_STATUS: &str = "/upgrade-mode-status"; + + absolute_route!( + upgrade_mode_status_absolute, + network_absolute(), + UPGRADE_MODE_STATUS + ); + } } } } diff --git a/nym-node/src/cli/commands/run/mod.rs b/nym-node/src/cli/commands/run/mod.rs index 36f77c7b29..450a88a761 100644 --- a/nym-node/src/cli/commands/run/mod.rs +++ b/nym-node/src/cli/commands/run/mod.rs @@ -29,7 +29,7 @@ fn check_public_ips(ips: &[IpAddr], local: bool) -> Result<(), NymNodeError> { warn!("\n##### WARNING #####"); for ip in suspicious_ip { warn!( - "The 'public' IP address you're trying to announce: {ip} may not be accessible to other clients.\ + "The 'public' IP address you're trying to announce: {ip} may not be accessible to other clients. \ Please make sure this is what you intended to announce.\ You can ignore this warning if you're running setup on a local network " ) diff --git a/nym-node/src/cli/helpers.rs b/nym-node/src/cli/helpers.rs index e7eb81623f..08ccef0857 100644 --- a/nym-node/src/cli/helpers.rs +++ b/nym-node/src/cli/helpers.rs @@ -9,6 +9,7 @@ use crate::error::NymNodeError; use celes::Country; use clap::Args; use clap::builder::ArgPredicate; +use nym_crypto::asymmetric::ed25519; use std::net::{IpAddr, SocketAddr}; use std::path::{Path, PathBuf}; use url::Url; @@ -429,12 +430,22 @@ pub(crate) struct EntryGatewayArgs { pub(crate) mnemonic: Option, /// Endpoint to query to retrieve current upgrade mode attestation. + /// This argument should never be set outside testnets and local networks. #[clap( long, env = NYMNODE_UPGRADE_MODE_ATTESTATION_URL_ARG )] #[zeroize(skip)] pub(crate) upgrade_mode_attestation_url: Option, + + /// Expected public key of the entity signing the published attestation. + /// This argument should never be set outside testnets and local networks. + #[clap( + long, + env = NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY_ARG + )] + #[zeroize(skip)] + pub(crate) upgrade_mode_attester_public_key: Option, } impl EntryGatewayArgs { @@ -465,6 +476,9 @@ impl EntryGatewayArgs { if let Some(upgrade_mode_attestation_url) = self.upgrade_mode_attestation_url.take() { section.upgrade_mode.attestation_url = upgrade_mode_attestation_url } + if let Some(upgrade_mode_attester_public_key) = self.upgrade_mode_attester_public_key { + section.upgrade_mode.attester_public_key = upgrade_mode_attester_public_key + } section } diff --git a/nym-node/src/config/gateway_tasks.rs b/nym-node/src/config/gateway_tasks.rs index 16517b77bc..ca47c15538 100644 --- a/nym-node/src/config/gateway_tasks.rs +++ b/nym-node/src/config/gateway_tasks.rs @@ -244,6 +244,7 @@ pub struct UpgradeModeWatcher { #[serde(with = "bs58_ed25519_pubkey")] pub attester_public_key: ed25519::PublicKey, + #[serde(default)] pub debug: UpgradeModeWatcherDebug, } diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index edec2a80af..08b578760e 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -695,6 +695,10 @@ impl ReplayProtectionDebug { pub const DEFAULT_BLOOMFILTER_MINIMUM_PACKETS_PER_SECOND_SIZE: usize = 200; pub fn validate(&self) -> Result<(), NymNodeError> { + if self.unsafe_disabled { + return Ok(()); + } + if self.false_positive_rate >= 1.0 || self.false_positive_rate <= 0.0 { return Err(NymNodeError::config_validation_failure( "false positive rate for replay detection can't be larger than (or equal to) 1 or smaller than (or equal to) 0", diff --git a/nym-node/src/env.rs b/nym-node/src/env.rs index 8f9a6d65b8..1564d087a4 100644 --- a/nym-node/src/env.rs +++ b/nym-node/src/env.rs @@ -63,6 +63,8 @@ pub mod vars { pub const NYMNODE_MNEMONIC_ARG: &str = "NYMNODE_MNEMONIC"; pub const NYMNODE_UPGRADE_MODE_ATTESTATION_URL_ARG: &str = "NYMNODE_UPGRADE_MODE_ATTESTATION_URL"; + pub const NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY_ARG: &str = + "NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY"; // exit gateway: pub const NYMNODE_UPSTREAM_EXIT_POLICY_ARG: &str = "NYMNODE_UPSTREAM_EXIT_POLICY"; diff --git a/nym-node/src/node/http/router/api/v1/mod.rs b/nym-node/src/node/http/router/api/v1/mod.rs index d0f2239f1d..b9a74b27bd 100644 --- a/nym-node/src/node/http/router/api/v1/mod.rs +++ b/nym-node/src/node/http/router/api/v1/mod.rs @@ -14,6 +14,7 @@ pub mod ip_packet_router; pub mod load; pub mod metrics; pub mod mixnode; +pub mod network; pub mod network_requester; pub mod node; pub mod openapi; @@ -34,6 +35,7 @@ pub(super) fn routes(config: Config) -> Router { Router::new() .route(v1::HEALTH, get(health::root_health)) .route(v1::LOAD, get(load::root_load)) + .nest(v1::NETWORK, network::routes()) .nest(v1::METRICS, metrics::routes(config.metrics)) .nest(v1::BRIDGES, bridges::routes(config.bridges)) .nest(v1::GATEWAY, gateway::routes(config.gateway)) diff --git a/nym-node/src/node/http/router/api/v1/network/mod.rs b/nym-node/src/node/http/router/api/v1/network/mod.rs new file mode 100644 index 0000000000..2d619eba3c --- /dev/null +++ b/nym-node/src/node/http/router/api/v1/network/mod.rs @@ -0,0 +1,14 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::http::api::v1::network::upgrade_mode::upgrade_mode_status; +use crate::node::http::state::AppState; +use axum::Router; +use axum::routing::get; +use nym_node_requests::routes::api::v1::network; + +pub mod upgrade_mode; + +pub(crate) fn routes() -> Router { + Router::new().route(network::UPGRADE_MODE_STATUS, get(upgrade_mode_status)) +} diff --git a/nym-node/src/node/http/router/api/v1/network/upgrade_mode.rs b/nym-node/src/node/http/router/api/v1/network/upgrade_mode.rs new file mode 100644 index 0000000000..f487c9a4fe --- /dev/null +++ b/nym-node/src/node/http/router/api/v1/network/upgrade_mode.rs @@ -0,0 +1,39 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::http::state::AppState; +use axum::extract::{Query, State}; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use nym_node_requests::api::v1::network::models::UpgradeModeStatus; + +/// Returns current upgrade mode information as perceived by this node. +#[utoipa::path( + get, + path = "/upgrade-mode-status", + context_path = "/api/v1/network", + tag = "Network", + responses( + (status = 200, content( + (UpgradeModeStatus = "application/json"), + (UpgradeModeStatus = "application/yaml"), + (UpgradeModeStatus = "application/bincode") + )) + ), + params(OutputParams) +)] +pub(crate) async fn upgrade_mode_status( + Query(output): Query, + State(state): State, +) -> UpgradeModeStatusResponse { + let output = output.get_output(); + let um_state = &state.upgrade_mode_state.node_state; + output.to_response(UpgradeModeStatus { + enabled: um_state.upgrade_mode_enabled(), + last_queried: um_state.last_queried(), + attestation_provider: state.upgrade_mode_state.attestation_url.clone(), + attester_pubkey: um_state.attester_pubkey(), + published_attestation: um_state.attestation().await, + }) +} + +pub type UpgradeModeStatusResponse = FormattedResponse; diff --git a/nym-node/src/node/http/router/api/v1/openapi.rs b/nym-node/src/node/http/router/api/v1/openapi.rs index 98835e4c59..ac410aba5f 100644 --- a/nym-node/src/node/http/router/api/v1/openapi.rs +++ b/nym-node/src/node/http/router/api/v1/openapi.rs @@ -1,7 +1,6 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node::http::router::api; use axum::Router; use nym_node_requests::api as api_requests; use nym_node_requests::routes::api::{v1, v1_absolute}; @@ -13,25 +12,31 @@ use utoipa_swagger_ui::SwaggerUi; #[openapi( info(title = "NymNode API"), paths( - api::v1::node::build_information::build_information, - api::v1::node::host_information::host_information, - api::v1::node::roles::roles, - api::v1::node::hardware::host_system, - api::v1::node::description::description, - api::v1::node::auxiliary::auxiliary, - api::v1::metrics::legacy_mixing::legacy_mixing_stats, - api::v1::metrics::packets_stats::packets_stats, - api::v1::metrics::verloc::verloc_stats, - api::v1::metrics::prometheus::prometheus_metrics, - api::v1::health::root_health, - api::v1::load::root_load, - api::v1::gateway::root::root_gateway, - api::v1::gateway::client_interfaces::client_interfaces, - api::v1::gateway::client_interfaces::mixnet_websockets, - api::v1::mixnode::root::root_mixnode, - api::v1::network_requester::root::root_network_requester, - api::v1::network_requester::exit_policy::node_exit_policy, - api::v1::ip_packet_router::root::root_ip_packet_router, + crate::node::http::router::api::v1::metrics::verloc::verloc_stats, + crate::node::http::router::api::v1::metrics::legacy_mixing::legacy_mixing_stats, + crate::node::http::router::api::v1::metrics::wireguard::wireguard_stats, + crate::node::http::router::api::v1::metrics::sessions::sessions_stats, + crate::node::http::router::api::v1::metrics::packets_stats::packets_stats, + crate::node::http::router::api::v1::metrics::prometheus::prometheus_metrics, + crate::node::http::router::api::v1::ip_packet_router::root::root_ip_packet_router, + crate::node::http::router::api::v1::health::root_health, + crate::node::http::router::api::v1::network::upgrade_mode::upgrade_mode_status, + crate::node::http::router::api::v1::mixnode::root::root_mixnode, + crate::node::http::router::api::v1::load::root_load, + crate::node::http::router::api::v1::node::host_information::host_information, + crate::node::http::router::api::v1::node::description::description, + crate::node::http::router::api::v1::node::roles::roles, + crate::node::http::router::api::v1::node::auxiliary::auxiliary, + crate::node::http::router::api::v1::node::build_information::build_information, + crate::node::http::router::api::v1::node::hardware::host_system, + crate::node::http::router::api::v1::authenticator::root::root_authenticator, + crate::node::http::router::api::v1::network_requester::exit_policy::node_exit_policy, + crate::node::http::router::api::v1::network_requester::root::root_network_requester, + crate::node::http::router::api::v1::gateway::client_interfaces::client_interfaces, + crate::node::http::router::api::v1::gateway::client_interfaces::mixnet_websockets, + crate::node::http::router::api::v1::gateway::client_interfaces::wireguard_details, + crate::node::http::router::api::v1::gateway::root::root_gateway, + ), components( schemas( diff --git a/nym-node/src/node/http/state/mod.rs b/nym-node/src/node/http/state/mod.rs index 9cd3348cb6..8ed756aa60 100644 --- a/nym-node/src/node/http/state/mod.rs +++ b/nym-node/src/node/http/state/mod.rs @@ -4,6 +4,7 @@ use crate::node::http::state::load::CachedNodeLoad; use crate::node::http::state::metrics::MetricsAppState; use crate::node::key_rotation::active_keys::ActiveSphinxKeys; +use nym_credential_verification::UpgradeModeState; use nym_crypto::asymmetric::ed25519; use nym_node_metrics::NymNodeMetrics; use nym_noise_keys::VersionedNoiseKey; @@ -12,6 +13,7 @@ use std::net::IpAddr; use std::sync::Arc; use std::time::Duration; use tokio::time::Instant; +use url::Url; pub mod load; pub mod metrics; @@ -23,6 +25,12 @@ pub(crate) struct StaticNodeInformation { pub(crate) hostname: Option, } +#[derive(Clone)] +pub(crate) struct UpgradeModeApiState { + pub(crate) node_state: UpgradeModeState, + pub(crate) attestation_url: Url, +} + #[derive(Clone)] pub(crate) struct AppState { pub(crate) startup_time: Instant, @@ -34,15 +42,18 @@ pub(crate) struct AppState { pub(crate) cached_load: CachedNodeLoad, pub(crate) metrics: MetricsAppState, + + pub(crate) upgrade_mode_state: UpgradeModeApiState, } impl AppState { - #[allow(clippy::new_without_default)] pub(crate) fn new( static_information: StaticNodeInformation, x25519_sphinx_keys: ActiveSphinxKeys, metrics: NymNodeMetrics, verloc: SharedVerlocStats, + upgrade_mode_attestation_url: Url, + upgrade_mode_state: UpgradeModeState, load_cache_ttl: Duration, ) -> Self { AppState { @@ -56,6 +67,10 @@ impl AppState { startup_time: Instant::now(), cached_load: CachedNodeLoad::new(load_cache_ttl), metrics: MetricsAppState { metrics, verloc }, + upgrade_mode_state: UpgradeModeApiState { + node_state: upgrade_mode_state, + attestation_url: upgrade_mode_attestation_url, + }, } } } diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index c0f94eb6da..8ba968ca29 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -39,6 +39,7 @@ use crate::node::shared_network::{ CachedNetwork, CachedTopologyProvider, LocalGatewayNode, NetworkRefresher, }; use nym_bin_common::bin_info; +use nym_credential_verification::UpgradeModeState; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_gateway::node::{ActiveClientsStore, GatewayTasksBuilder, UpgradeModeCheckRequestSender}; use nym_mixnet_client::client::ActiveConnections; @@ -373,6 +374,8 @@ pub(crate) struct NymNode { entry_gateway: GatewayTasksData, + upgrade_mode_state: UpgradeModeState, + #[allow(dead_code)] service_providers: ServiceProvidersData, @@ -461,6 +464,9 @@ impl NymNode { metrics: NymNodeMetrics::new(), verloc_stats: Default::default(), entry_gateway: GatewayTasksData::new(&config.gateway_tasks).await?, + upgrade_mode_state: UpgradeModeState::new( + config.gateway_tasks.upgrade_mode.attester_public_key, + ), service_providers: ServiceProvidersData::new(&config.service_providers)?, wireguard: Some(wireguard_data), config, @@ -625,7 +631,7 @@ impl NymNode { self.metrics.clone(), self.entry_gateway.mnemonic.clone(), Self::user_agent(), - self.config.gateway_tasks.upgrade_mode.attester_public_key, + self.upgrade_mode_state.clone(), self.shutdown_tracker().clone(), ); @@ -861,6 +867,12 @@ impl NymNode { self.active_sphinx_keys()?.clone(), self.metrics.clone(), self.verloc_stats.clone(), + self.config + .gateway_tasks + .upgrade_mode + .attestation_url + .clone(), + self.upgrade_mode_state.clone(), self.config.http.node_load_cache_ttl, ); @@ -1152,7 +1164,7 @@ impl NymNode { } pub(crate) async fn run_minimal_mixnet_processing(mut self) -> Result<(), NymNodeError> { - let noise_config = nym_noise::config::NoiseConfig::new( + let noise_config = NoiseConfig::new( self.x25519_noise_keys.clone(), NoiseNetworkView::new_empty(), self.config.mixnet.debug.initial_connection_timeout, diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 120e1cc969..4f910e1a15 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -4466,13 +4466,13 @@ version = "0.1.0" dependencies = [ "async-trait", "celes", - "humantime 2.2.0", "humantime-serde", "nym-bin-common", "nym-crypto", "nym-exit-policy", "nym-http-api-client", "nym-noise-keys", + "nym-upgrade-mode-check", "nym-wireguard-types", "schemars", "serde", @@ -4481,6 +4481,7 @@ dependencies = [ "strum_macros", "thiserror 2.0.12", "time", + "url", "utoipa", ] @@ -4606,6 +4607,7 @@ dependencies = [ "thiserror 2.0.12", "time", "tracing", + "utoipa", ] [[package]] diff --git a/tools/echo-server/Cargo.toml b/tools/echo-server/Cargo.toml index 390f746bcc..f58a0e46b3 100644 --- a/tools/echo-server/Cargo.toml +++ b/tools/echo-server/Cargo.toml @@ -26,7 +26,7 @@ serde = { workspace = true, features = ["derive"] } tracing.workspace = true tracing-subscriber = { workspace = true } bytecodec = { workspace = true } -nym-sdk = { path = "../../sdk/rust/nym-sdk/" } +nym-sdk = { path = "../../sdk/rust/nym-sdk" } bytes.workspace = true dirs.workspace = true clap.workspace = true diff --git a/tools/internal/testnet-manager/src/manager/local_client.rs b/tools/internal/testnet-manager/src/manager/local_client.rs index 8e3aff09c0..3969bfea33 100644 --- a/tools/internal/testnet-manager/src/manager/local_client.rs +++ b/tools/internal/testnet-manager/src/manager/local_client.rs @@ -213,12 +213,14 @@ impl NetworkManager { id, "--enabled-credentials-mode", "true", + "--minimum-gateway-performance", + "0", "--port", &port.to_string(), ]) - .stdout(Stdio::null()) + // .stdout(Stdio::null()) .stdin(Stdio::null()) - .stderr(Stdio::null()) + // .stderr(Stdio::null()) .kill_on_drop(true); if let Some(gateway) = &ctx.gateway { @@ -243,10 +245,10 @@ impl NetworkManager { config_file, r#" -[debug.topology] -minimum_mixnode_performance = 0 -minimum_gateway_performance = 0 -"# + [debug.topology] + minimum_mixnode_performance = 0 + minimum_gateway_performance = 0 + "# )?; ctx.println(format!("\t✅client {id} is ready to use!")); diff --git a/tools/internal/testnet-manager/src/manager/local_nodes.rs b/tools/internal/testnet-manager/src/manager/local_nodes.rs index f710939248..3ed0af70b3 100644 --- a/tools/internal/testnet-manager/src/manager/local_nodes.rs +++ b/tools/internal/testnet-manager/src/manager/local_nodes.rs @@ -577,7 +577,7 @@ impl NetworkManager { )); let id = ctx.nym_node_id(mixnode); cmds.push(format!( - "{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise" + "{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise --unsafe-disable-replay-protection" )); } @@ -588,7 +588,7 @@ impl NetworkManager { )); let id = ctx.nym_node_id(gateway); cmds.push(format!( - "{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise" + "{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise --unsafe-disable-replay-protection" )); } From 6acc54d2bcff46fc3f8f070f17a5ab373d2cd51a Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:03:36 +0100 Subject: [PATCH 085/180] syntax fix --- scripts/nym-node-setup/network-tunnel-manager.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 668b6c55f1..acd0186366 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -41,7 +41,9 @@ WG_INTERFACE="${WG_INTERFACE:-nymwg}" detect_uplink_interface() { local cmd="$1" local dev - dev="$(eval "$cmd" 2>/dev/null | awk '{print \$5}' | head -n1 || true)" + + dev="$(eval "$cmd" 2>/dev/null | awk '{print $5}' | head -n1 || true)" + if [[ -n "$dev" && "$dev" =~ ^[a-zA-Z0-9._-]+$ ]]; then echo "$dev" else @@ -49,6 +51,7 @@ detect_uplink_interface() { fi } + # uplink device detection, can be overridden NETWORK_DEVICE="${NETWORK_DEVICE:-}" if [[ -z "$NETWORK_DEVICE" ]]; then From e5aef76256a9fd42957392977a9719464726190d Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:27:44 +0100 Subject: [PATCH 086/180] non-interactive --- scripts/nym-node-setup/quic_bridge_deployment.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/quic_bridge_deployment.sh b/scripts/nym-node-setup/quic_bridge_deployment.sh index 2d6b151feb..db14127016 100644 --- a/scripts/nym-node-setup/quic_bridge_deployment.sh +++ b/scripts/nym-node-setup/quic_bridge_deployment.sh @@ -75,9 +75,15 @@ err() { echo -e "${RED}$1${RESET}"; } info() { echo -e "${CYAN}$1${RESET}"; } press_enter() { read -r -p "$1"; } -# Disable pauses for noninteractive mode +# Disable pauses and interactive prompts for noninteractive mode if [[ "${NONINTERACTIVE:-0}" == "1" ]]; then + # all pauses become no-ops press_enter() { :; } + + # silence any "enter path:" prompts + echo_prompt() { :; } +else + echo_prompt() { echo -n "$1"; } fi # Helper: detect dpkg dependency failure for libc6>=2.34 From 440aadf1245d081be8f6bc8b9a9c7d2db6737403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 14 Nov 2025 15:04:51 +0000 Subject: [PATCH 087/180] chore: updated default endpoint for retrieving attestation.json (#6207) --- common/network-defaults/src/mainnet.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index 291f397482..8e253a77b7 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -55,7 +55,8 @@ pub const NYM_APIS: &[ApiUrlConst] = &[ pub const NYM_VPN_API: &str = "https://nymvpn.com/api/"; -pub const UPGRADE_MODE_ATTESTATION_URL: &str = "https://nym.com/upgrade-mode/attestation.json"; +pub const UPGRADE_MODE_ATTESTATION_URL: &str = + "https://nymtech.net/.wellknown/upgrade-mode/attestation.json"; pub const UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY: &str = "3bgffBYcfFkTTXc2npNNn9MkddFZ3H2LrPjXDmnJzrqd"; From d73b7b712753a77d37c85b58074e812cd58003c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 14 Nov 2025 15:04:59 +0000 Subject: [PATCH 088/180] chore: remove support for legacy mixnode within the performance contract (#6205) --- contracts/performance/src/helpers.rs | 23 +---- contracts/performance/src/storage.rs | 105 -------------------- contracts/performance/src/testing/mod.rs | 116 +---------------------- 3 files changed, 6 insertions(+), 238 deletions(-) diff --git a/contracts/performance/src/helpers.rs b/contracts/performance/src/helpers.rs index 51ccb4f4b2..8219910787 100644 --- a/contracts/performance/src/helpers.rs +++ b/contracts/performance/src/helpers.rs @@ -3,7 +3,7 @@ use cosmwasm_std::{from_json, Binary, CustomQuery, QuerierWrapper, StdError, StdResult}; use cw_storage_plus::{Key, Namespace, Path, PrimaryKey}; -use nym_mixnet_contract_common::{Interval, MixNodeBond, NymNodeBond}; +use nym_mixnet_contract_common::{Interval, NymNodeBond}; use nym_performance_contract_common::{EpochId, NodeId}; use serde::de::DeserializeOwned; use std::ops::Deref; @@ -51,15 +51,10 @@ pub(crate) trait MixnetContractQuerier { fn check_node_existence(&self, address: impl Into, node_id: NodeId) -> StdResult { let mixnet_contract_address = address.into(); - // 1. check if it's a nym-node if let Some(nym_node) = self.query_nymnode_bond(mixnet_contract_address.clone(), node_id)? { return Ok(!nym_node.is_unbonding); } - // 2. try a legacy mixnode - if let Some(nym_node) = self.query_mixnode_bond(mixnet_contract_address, node_id)? { - return Ok(!nym_node.is_unbonding); - } Ok(false) } @@ -78,22 +73,6 @@ pub(crate) trait MixnetContractQuerier { self.query_mixnet_contract_storage_value(address, storage_key) } - - fn query_mixnode_bond( - &self, - address: impl Into, - node_id: NodeId, - ) -> StdResult> { - // construct proper map key - let pk_namespace = "mnn"; - let path: Path = Path::new( - Namespace::from_static_str(pk_namespace).as_slice(), - &node_id.key().iter().map(Key::as_ref).collect::>(), - ); - let storage_key = path.deref(); - - self.query_mixnet_contract_storage_value(address, storage_key) - } } impl MixnetContractQuerier for QuerierWrapper<'_, C> diff --git a/contracts/performance/src/storage.rs b/contracts/performance/src/storage.rs index acba90762b..e392fb78ba 100644 --- a/contracts/performance/src/storage.rs +++ b/contracts/performance/src/storage.rs @@ -1261,28 +1261,6 @@ mod tests { } ); - // bonded legacy mix-node - let node_id = tester.bond_dummy_legacy_mixnode()?; - let perf = NodePerformance { - node_id, - performance: Default::default(), - }; - let res = - storage.submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, perf); - assert!(res.is_ok()); - - // unbonded - tester.unbond_legacy_mixnode(node_id)?; - - let res = storage - .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, dummy_perf) - .unwrap_err(); - assert_eq!( - res, - NymPerformanceContractError::NodeNotBonded { - node_id: dummy_perf.node_id - } - ); Ok(()) } } @@ -1984,11 +1962,6 @@ mod tests { tester.unbond_nymnode(nym_node_between)?; let nym_node2 = tester.bond_dummy_nymnode()?; - let mix_node1 = tester.bond_dummy_legacy_mixnode()?; - let mixnode_between = tester.bond_dummy_legacy_mixnode()?; - tester.unbond_legacy_mixnode(mixnode_between)?; - let mix_node2 = tester.bond_dummy_legacy_mixnode()?; - let env = tester.env(); // single id - nothing bonded @@ -2053,84 +2026,6 @@ mod tests { assert_eq!(res.accepted_scores, 2); assert_eq!(res.non_existent_nodes, vec![2, nym_node_between]); - // MIXNODES - - // one bonded mixnode, one not bonded - let res = storage.batch_submit_performance_results( - tester.deps_mut(), - env.clone(), - &nm, - 3, - vec![ - NodePerformance { - node_id: mix_node1, - performance: Default::default(), - }, - NodePerformance { - node_id: 999999, - performance: Default::default(), - }, - ], - )?; - assert_eq!(res.accepted_scores, 1); - assert_eq!(res.non_existent_nodes, vec![999999]); - - // not-bonded, bonded, not-bonded, bonded - let res = storage.batch_submit_performance_results( - tester.deps_mut(), - env.clone(), - &nm, - 4, - vec![ - NodePerformance { - node_id: 2, - performance: Default::default(), - }, - NodePerformance { - node_id: mix_node1, - performance: Default::default(), - }, - NodePerformance { - node_id: mixnode_between, - performance: Default::default(), - }, - NodePerformance { - node_id: mix_node2, - performance: Default::default(), - }, - ], - )?; - assert_eq!(res.accepted_scores, 2); - assert_eq!(res.non_existent_nodes, vec![2, mixnode_between]); - - // nym-node, not bonded, mixnode - let res = storage.batch_submit_performance_results( - tester.deps_mut(), - env.clone(), - &nm, - 5, - vec![ - NodePerformance { - node_id: 3, - performance: Default::default(), - }, - NodePerformance { - node_id: nym_node1, - performance: Default::default(), - }, - NodePerformance { - node_id: nym_node_between, - performance: Default::default(), - }, - NodePerformance { - node_id: mix_node2, - performance: Default::default(), - }, - ], - )?; - assert_eq!(res.accepted_scores, 2); - assert_eq!(res.non_existent_nodes, vec![3, nym_node_between]); - Ok(()) } } diff --git a/contracts/performance/src/testing/mod.rs b/contracts/performance/src/testing/mod.rs index 614982acc3..33e90beaf3 100644 --- a/contracts/performance/src/testing/mod.rs +++ b/contracts/performance/src/testing/mod.rs @@ -6,10 +6,9 @@ use crate::helpers::MixnetContractQuerier; use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE; use cosmwasm_std::testing::{message_info, mock_env, MockApi}; use cosmwasm_std::{ - coin, coins, Addr, Binary, ContractInfo, Deps, DepsMut, Env, MessageInfo, QuerierWrapper, - StdError, StdResult, + coin, coins, Addr, ContractInfo, Deps, DepsMut, Env, MessageInfo, QuerierWrapper, StdError, + StdResult, }; -use cw_storage_plus::PrimaryKey; use mixnet_contract::testable_mixnet_contract::MixnetContract; use nym_contracts_common::signing::{ContractMessageContent, MessageSignature}; use nym_contracts_common::Percent; @@ -22,9 +21,8 @@ use nym_contracts_common_testing::{ use nym_crypto::asymmetric::ed25519; use nym_mixnet_contract_common::nym_node::{NodeDetailsResponse, NodeOwnershipResponse, Role}; use nym_mixnet_contract_common::{ - CurrentIntervalResponse, EpochId, Interval, MixNode, MixNodeBond, MixnodeDetailsResponse, - NodeCostParams, NodeRewarding, NymNode, NymNodeBondingPayload, RoleAssignment, - SignableNymNodeBondingMsg, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, + CurrentIntervalResponse, EpochId, Interval, NodeCostParams, NymNode, NymNodeBondingPayload, + RoleAssignment, SignableNymNodeBondingMsg, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, }; use nym_performance_contract_common::constants::storage_keys; @@ -33,7 +31,7 @@ use nym_performance_contract_common::{ NymPerformanceContractError, QueryMsg, }; use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use std::str::FromStr; pub struct PerformanceContract; @@ -499,110 +497,6 @@ pub(crate) trait PerformanceContractTesterExt: self.advance_mixnet_epoch()?; Ok(()) } - - fn bond_dummy_legacy_mixnode(&mut self) -> Result { - #[derive(Deserialize, Serialize)] - pub(crate) struct UniqueRef { - // note, we collapse the pk - combining everything under the namespace - even if it is composite - pk: Binary, - value: T, - } - - // there's no proper Execute flow for this anymore, so we have to "hack" the storage a bit, - // ensuring all invariants still hold - let owner = self.generate_account_with_balance(); - - let mixnode = MixNode { - host: "1.2.3.4".to_string(), - mix_port: 123, - verloc_port: 123, - http_api_port: 123, - sphinx_key: "aaaa".to_string(), - identity_key: "bbbbb".to_string(), - version: "ccc".to_string(), - }; - let cost_params = NodeCostParams { - profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT) - .unwrap(), - interval_operating_cost: coin(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, TEST_DENOM), - }; - - // adjust node counter - let node_id_counter: u32 = self.read_from_mixnet_contract_storage("nic")?; - let node_id = node_id_counter + 1; - self.write_to_mixnet_contract_storage_value("nic", &node_id)?; - - let current_epoch = self.current_mixnet_epoch()?; - let pledge = coin(100_000000, TEST_DENOM); - let mixnode_rewarding = - NodeRewarding::initialise_new(cost_params, &pledge, current_epoch).unwrap(); - let env = self.env(); - let mixnode_bond = MixNodeBond { - mix_id: node_id, - owner, - original_pledge: pledge, - mix_node: mixnode, - proxy: None, - bonding_height: env.block.height, - is_unbonding: false, - }; - - // save to the main mixnode storage - self.set_contract_map_value( - self.mixnet_contract_address()?, - "mnn", - node_id, - &mixnode_bond, - )?; - // update indices - let pk = node_id.joined_key(); - let unique_ref = UniqueRef { - pk: pk.into(), - value: mixnode_bond.clone(), - }; - - // owner index - let idx = mixnode_bond.owner.clone(); - self.set_contract_map_value(self.mixnet_contract_address()?, "mno", idx, &unique_ref)?; - - // identity key index - let idx = mixnode_bond.mix_node.identity_key.clone(); - self.set_contract_map_value(self.mixnet_contract_address()?, "mni", idx, &unique_ref)?; - - // sphinx key index - let idx = mixnode_bond.mix_node.sphinx_key.clone(); - self.set_contract_map_value(self.mixnet_contract_address()?, "mns", idx, &unique_ref)?; - - // update rewarding data - self.set_contract_map_value( - self.mixnet_contract_address()?, - "mnr", - node_id, - &mixnode_rewarding, - )?; - - Ok(node_id) - } - - fn unbond_legacy_mixnode( - &mut self, - node_id: NodeId, - ) -> Result<(), NymPerformanceContractError> { - let bond: MixnodeDetailsResponse = self.query_arbitrary_contract( - self.mixnet_contract_address()?, - &nym_mixnet_contract_common::QueryMsg::GetMixnodeDetails { mix_id: node_id }, - )?; - - let node_owner = bond.mixnode_details.unwrap().bond_information.owner; - - self.execute_mixnet_contract( - message_info(&node_owner, &[]), - &nym_mixnet_contract_common::ExecuteMsg::UnbondMixnode {}, - )?; - - self.advance_mixnet_epoch()?; - Ok(()) - } } impl PerformanceContractTesterExt for ContractTester {} From 4f991061dde3a87078b3da6868369f583ed59193 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Fri, 14 Nov 2025 16:22:58 +0100 Subject: [PATCH 089/180] fix nginx errors --- .../nym-node-setup/setup-nginx-proxy-wss.sh | 164 +++++++----------- 1 file changed, 65 insertions(+), 99 deletions(-) diff --git a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh index b72cf6ed24..cf1378f5b6 100644 --- a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh +++ b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh @@ -23,50 +23,28 @@ SITES_AVAIL="/etc/nginx/sites-available" SITES_EN="/etc/nginx/sites-enabled" HTTP_CONF="${SITES_AVAIL}/${HOSTNAME}" -HTTPS_CONF="${SITES_AVAIL}/${HOSTNAME}-ssl" WSS_CONF="${SITES_AVAIL}/wss-config-nym" echo -echo "* * * Starting clean nginx configuration for landing page, reverse proxy and wss * * *" - -################################################################################ -# step 1: clean all previous configs -################################################################################ - -echo "cleaning existing nginx configuration" - -# remove default nginx config -[[ -L "${SITES_EN}/default" ]] && unlink "${SITES_EN}/default" || true - -# remove domain symlinks -rm -f "${SITES_EN}/${HOSTNAME}" || true -rm -f "${SITES_EN}/${HOSTNAME}-ssl" || true -rm -f "${SITES_EN}/wss-config-nym" || true - -# remove old configs -rm -f "${HTTP_CONF}" || true -rm -f "${HTTPS_CONF}" || true -rm -f "${WSS_CONF}" || true +echo "* * * Starting nginx configuration for landing page, reverse proxy and WSS * * *" ############################################################################### -# step 2: create landing page +# step 1: ensure landing page exists (local fetch -> github -> template) ############################################################################### mkdir -p "${WEBROOT}" -# script directory where Python CLI stores fetched scripts SCRIPT_DIR="$(dirname "${ENV_FILE:-./env.sh}")" LOCAL_FETCHED_PAGE="${SCRIPT_DIR}/landing-page.html" if [[ -s "${LOCAL_FETCHED_PAGE}" ]]; then cp "${LOCAL_FETCHED_PAGE}" "${WEBROOT}/index.html" +elif curl -fsSL \ + https://raw.githubusercontent.com/nymtech/nym/develop/scripts/nym-node-setup/landing-page.html \ + -o "${WEBROOT}/index.html"; then + : else - if curl -fsSL \ - https://raw.githubusercontent.com/nymtech/nym/develop/scripts/nym-node-setup/landing-page.html \ - -o "${WEBROOT}/index.html"; then - : - else - cat > "${WEBROOT}/index.html" < "${WEBROOT}/index.html" < nym node @@ -77,16 +55,35 @@ else EOF - fi fi -echo "landing page at ${WEBROOT}/index.html" +echo "Landing page at ${WEBROOT}/index.html" -echo "landing page at ${WEBROOT}/index.html" +############################################################################### +# step 2: remove default site and old configs, restart nginx +############################################################################### -################################################################################ -# step 3: HTTP :80 config -################################################################################ +echo "Cleaning existing nginx configuration" + +# remove default nginx site +[[ -L "${SITES_EN}/default" ]] && unlink "${SITES_EN}/default" || true + +# optional: remove default available config if present +rm -f /etc/nginx/sites-available/default || true + +# remove old vhosts for this domain +rm -f "${SITES_EN}/${HOSTNAME}" || true +rm -f "${SITES_EN}/${HOSTNAME}-ssl" || true +rm -f "${SITES_EN}/wss-config-nym" || true + +rm -f "${HTTP_CONF}" || true +rm -f "${WSS_CONF}" || true + +systemctl restart nginx || systemctl start nginx + +############################################################################### +# step 3: create basic HTTP config like manual flow (80 -> 8080) +############################################################################### cat > "${HTTP_CONF}" </dev/null 2>&1 || true apt-get install -y certbot python3-certbot-nginx >/dev/null 2>&1 || true -echo "requesting let's encrypt certificate for ${HOSTNAME}" +echo "Requesting Let's Encrypt certificate for ${HOSTNAME}" -certbot --nginx --non-interactive --agree-tos \ - --reuse-key \ - -m "${EMAIL}" -d "${HOSTNAME}" --redirect || true +certbot --nginx --non-interactive --agree-tos --redirect --reuse-key \ + -m "${EMAIL}" -d "${HOSTNAME}" || true -################################################################################ -# step 5: HTTPS and WSS configs -################################################################################ +############################################################################### +# step 5: create WSS 9001 config using certbot-generated certs +############################################################################### if [[ -s "/etc/letsencrypt/live/${HOSTNAME}/fullchain.pem" ]]; then - echo "certificate detected, creating https and wss configs" + echo "Certificate detected, creating WSS config" - # HTTPS 443 - cat > "${HTTPS_CONF}" < "${WSS_CONF}" < Date: Mon, 17 Nov 2025 09:18:01 +0000 Subject: [PATCH 090/180] Node Status UI (#6210) Co-authored-by: Mark Sinclair --- .../nym-node-status-ui/package.json | 2 + .../nym-node-status-ui/pnpm-lock.yaml | 11 + .../src/app/dvpn/GatewaysTable.tsx | 379 +++++++ .../nym-node-status-ui/src/app/dvpn/page.tsx | 15 + .../nym-node-status-ui/src/app/favicon.ico | Bin 0 -> 25931 bytes .../nym-node-status-ui/src/app/layout.tsx | 22 +- .../src/app/nodes/NodesTable.tsx | 124 +++ .../nym-node-status-ui/src/app/nodes/page.tsx | 15 + .../src/app/nym-vpn/GatewaysTable.tsx | 115 +++ .../src/app/nym-vpn/page.tsx | 15 + .../nym-node-status-ui/src/app/page.tsx | 56 +- .../src/app/socks5/page.tsx | 11 + .../src/app/validators/page.tsx | 11 + .../src/app/zk-nym-signers/page.tsx | 11 + .../src/client/@tanstack/react-query.gen.ts | 936 +++++++++++++++++ .../src/client/client.gen.ts | 28 + .../src/client/client/client.ts | 195 ++++ .../src/client/client/index.ts | 22 + .../src/client/client/types.ts | 219 ++++ .../src/client/client/utils.ts | 417 ++++++++ .../src/client/core/auth.ts | 40 + .../src/client/core/bodySerializer.ts | 88 ++ .../src/client/core/params.ts | 141 +++ .../src/client/core/pathSerializer.ts | 179 ++++ .../src/client/core/types.ts | 104 ++ .../nym-node-status-ui/src/client/index.ts | 3 + .../nym-node-status-ui/src/client/sdk.gen.ts | 395 ++++++++ .../src/client/types.gen.ts | 940 ++++++++++++++++++ .../src/components/CardAlert.tsx | 25 + .../src/components/Copyright.tsx | 24 + .../src/components/GraphCard.tsx | 23 + .../src/components/HighlightedCard.tsx | 42 + .../src/components/ScoreIcon.tsx | 36 + .../src/components/StatCard.tsx | 130 +++ .../graphs/GatewayCanQueryMetadataTopup.tsx | 58 ++ .../graphs/GatewayDownloadSpeeds.tsx | 43 + .../src/components/graphs/GatewayLoads.tsx | 45 + .../graphs/GatewayPingPercentage.tsx | 43 + .../src/components/graphs/GatewayScores.tsx | 45 + .../graphs/GatewayUptimePercentage.tsx | 41 + .../src/components/graphs/GatewayVersions.tsx | 49 + .../src/components/nav/AppNavbar.tsx | 77 ++ .../components/nav/ColorModeIconDropdown.tsx | 91 ++ .../src/components/nav/ColorModeSelect.tsx | 29 + .../src/components/nav/Header.tsx | 33 + .../src/components/nav/MenuButton.tsx | 23 + .../src/components/nav/MenuContent.tsx | 60 ++ .../src/components/nav/NavbarBreadcrumbs.tsx | 37 + .../src/components/nav/OptionsMenu.tsx | 81 ++ .../src/components/nav/Search.tsx | 27 + .../src/components/nav/SelectContent.tsx | 107 ++ .../src/components/nav/SideMenu.tsx | 56 ++ .../src/components/nav/SideMenuMobile.tsx | 43 + .../src/components/nav/SiteLogo.tsx | 15 + .../src/context/queryContext.tsx | 45 + .../nym-node-status-ui/src/hooks/types.ts | 4 + .../src/hooks/useAllNymNodes.ts | 39 + .../src/hooks/useGateways.ts | 20 + .../src/hooks/useGatewaysTransformed.ts | 47 + .../src/hooks/useNymNodes.ts | 52 + .../src/layouts/LayoutWithNav.tsx | 29 + .../src/layouts/NestedLayoutWithHeader.tsx | 23 + .../src/theme/ColorModeIconDropdown.tsx | 91 ++ .../nym-node-status-ui/src/theme/index.tsx | 27 + 64 files changed, 6152 insertions(+), 2 deletions(-) create mode 100644 nym-node-status-api/nym-node-status-ui/src/app/dvpn/GatewaysTable.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/app/dvpn/page.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/app/favicon.ico create mode 100644 nym-node-status-api/nym-node-status-ui/src/app/nodes/NodesTable.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/app/nodes/page.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/app/nym-vpn/GatewaysTable.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/app/nym-vpn/page.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/app/socks5/page.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/app/validators/page.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/app/zk-nym-signers/page.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/client/@tanstack/react-query.gen.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/client/client.gen.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/client/client/client.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/client/client/index.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/client/client/types.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/client/client/utils.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/client/core/auth.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/client/core/bodySerializer.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/client/core/params.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/client/core/pathSerializer.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/client/core/types.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/client/index.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/client/sdk.gen.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/client/types.gen.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/CardAlert.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/Copyright.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/GraphCard.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/HighlightedCard.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/ScoreIcon.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/StatCard.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayCanQueryMetadataTopup.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayDownloadSpeeds.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayLoads.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayPingPercentage.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayScores.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayUptimePercentage.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayVersions.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/nav/AppNavbar.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/nav/ColorModeIconDropdown.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/nav/ColorModeSelect.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/nav/Header.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/nav/MenuButton.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/nav/MenuContent.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/nav/NavbarBreadcrumbs.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/nav/OptionsMenu.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/nav/Search.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/nav/SelectContent.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/nav/SideMenu.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/nav/SideMenuMobile.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/components/nav/SiteLogo.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/context/queryContext.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/hooks/types.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/hooks/useAllNymNodes.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/hooks/useGateways.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/hooks/useGatewaysTransformed.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/hooks/useNymNodes.ts create mode 100644 nym-node-status-api/nym-node-status-ui/src/layouts/LayoutWithNav.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/layouts/NestedLayoutWithHeader.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/theme/ColorModeIconDropdown.tsx create mode 100644 nym-node-status-api/nym-node-status-ui/src/theme/index.tsx diff --git a/nym-node-status-api/nym-node-status-ui/package.json b/nym-node-status-api/nym-node-status-ui/package.json index 610a288446..d738ed90dc 100644 --- a/nym-node-status-api/nym-node-status-ui/package.json +++ b/nym-node-status-api/nym-node-status-ui/package.json @@ -19,6 +19,7 @@ "@mui/x-charts": "^8.8.0", "@mui/x-date-pickers": "^7.29.4", "@tanstack/react-query": "^5.83.0", + "d3-array": "^3.2.4", "dayjs": "^1.11.13", "material-react-table": "^3.2.1", "next": "^15.4.1", @@ -30,6 +31,7 @@ "devDependencies": { "@biomejs/biome": "^1.9.4", "@tanstack/react-query-devtools": "^5.83.0", + "@types/d3-array": "^3.2.2", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/nym-node-status-api/nym-node-status-ui/pnpm-lock.yaml b/nym-node-status-api/nym-node-status-ui/pnpm-lock.yaml index ab4a48acac..e42724493a 100644 --- a/nym-node-status-api/nym-node-status-ui/pnpm-lock.yaml +++ b/nym-node-status-api/nym-node-status-ui/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@tanstack/react-query': specifier: ^5.83.0 version: 5.83.0(react@19.0.0) + d3-array: + specifier: ^3.2.4 + version: 3.2.4 dayjs: specifier: ^1.11.13 version: 1.11.13 @@ -60,6 +63,9 @@ importers: '@tanstack/react-query-devtools': specifier: ^5.83.0 version: 5.83.0(@tanstack/react-query@5.83.0(react@19.0.0))(react@19.0.0) + '@types/d3-array': + specifier: ^3.2.2 + version: 3.2.2 '@types/node': specifier: ^20 version: 20.17.14 @@ -659,6 +665,9 @@ packages: '@tanstack/virtual-core@3.11.2': resolution: {integrity: sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + '@types/d3-color@3.1.3': resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} @@ -1723,6 +1732,8 @@ snapshots: '@tanstack/virtual-core@3.11.2': {} + '@types/d3-array@3.2.2': {} + '@types/d3-color@3.1.3': {} '@types/d3-delaunay@6.0.4': {} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/dvpn/GatewaysTable.tsx b/nym-node-status-api/nym-node-status-ui/src/app/dvpn/GatewaysTable.tsx new file mode 100644 index 0000000000..30a389cd80 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/dvpn/GatewaysTable.tsx @@ -0,0 +1,379 @@ +import type { DVpnGateway } from "@/client"; +import { ReverseScoreIcon, ScoreIcon } from "@/components/ScoreIcon"; +import { useDVpnGatewaysTransformed } from "@/hooks/useGatewaysTransformed"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import { + IconButton, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Tooltip, +} from "@mui/material"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; +import relativeTime from "dayjs/plugin/relativeTime"; +import { + type MRT_ColumnDef, + MaterialReactTable, + useMaterialReactTable, +} from "material-react-table"; +import { useMemo } from "react"; +import ReactCountryFlag from "react-country-flag"; + +dayjs.extend(duration); +dayjs.extend(relativeTime); + +const regionNamesInEnglish = new Intl.DisplayNames(["en"], { type: "region" }); + +const staleGatewayBinWidthMinutes = 15; + +interface StaleGatewayStats { + bins: number[]; + average: number; + sum: number; + count: number; +} + +export default function GatewaysTable() { + const { data, isError, isRefetching, isLoading, refetch } = + useDVpnGatewaysTransformed().query; + + const staleGateways = useMemo( + () => + (data || []).reduce( + (acc, g) => { + const last_updated_utc = g.last_probe + ? dayjs(g.last_probe.last_updated_utc) + : null; + if (!last_updated_utc) return acc; + const diff = dayjs().diff(last_updated_utc, "minutes"); + const bin = Math.floor(diff / staleGatewayBinWidthMinutes); + if (!acc.bins[bin]) { + acc.bins[bin] = 0; + } + acc.bins[bin] += 1; + acc.sum += diff; + acc.count += 1; + acc.average = acc.sum / acc.count; + return acc; + }, + { + bins: [], + average: 0, + sum: 0, + count: 0, + } as StaleGatewayStats, + ), + [data], + ); + + const columns = useMemo[]>( + //column definitions... + () => [ + { + accessorKey: "name", + header: "Name", + }, + { + accessorKey: "identity_key", + header: "Identity Key", + Cell: ({ cell }) => ( + {cell.getValue()?.slice(0, 8)}... + ), + }, + { + accessorKey: "location.two_letter_iso_country_code", + header: "Country", + Cell: ({ cell }) => { + const value = cell.getValue(); + return ( + <> + {value} + + {regionNamesInEnglish.of(value)} + + + ); + }, + }, + { + accessorKey: "location.region", + header: "City / Region", + Cell: ({ row }) => { + return ( + <> + + {(row.original.location as any).city}/ + {(row.original.location as any).region} + + + ); + }, + }, + { + accessorKey: "performance_v2.score", + width: 20, + header: "Score", + Cell: ({ cell }) => { + const value = cell.getValue(); + return ( + <> + + + {value || "-"} + + + ); + }, + }, + { + accessorKey: "performance_v2.load", + width: 20, + header: "Load", + Cell: ({ cell }) => { + const value = cell.getValue(); + return ( + <> + + + {value || "-"} + + + ); + }, + }, + { + accessorKey: "extra.downloadSpeedMBPerSec", + header: "Download Speed ipv4 (MB/sec)", + Cell: ({ renderedCellValue, cell }) => { + if (!cell.getValue()) { + return null; + } + return ( + + {renderedCellValue} MB/sec + + ); + }, + }, + { + accessorKey: "extra.downloadSpeedIpv6MBPerSec", + header: "Download Speed ipv6 (MB/sec)", + Cell: ({ renderedCellValue, cell }) => { + if (!cell.getValue()) { + return null; + } + return ( + + {renderedCellValue} MB/sec + + ); + }, + }, + { + accessorKey: "last_probe.outcome.wg.ping_ips_performance_v4", + header: "Probe pings (IPV4)", + Cell: ({ cell }) => { + const value = Math.floor( + Number.parseFloat(cell.getValue() || "0") * 100, + ); + return ( + <> + + {value}% + + + ); + }, + }, + { + accessorKey: "performance_v2.uptime_percentage_last_24_hours", + width: 20, + header: "Uptime", + Cell: ({ cell, row }) => { + const value: number = + ((row.original as any).performance_v2 + ?.uptime_percentage_last_24_hours || 0) * 100; + // const value = Math.floor(Number.parseFloat(cell.getValue()) * 100); + return ( + <> + + {value}% + + + ); + }, + }, + { + accessorKey: "last_probe.outcome.wg.can_query_metadata_v4", + header: "Can query metadata?", + Cell: ({ cell }) => { + const wg = cell.row.original.last_probe?.outcome.wg as any; + const can_query_metadata_v4 = wg?.can_query_metadata_v4; + return ( + <> + + {can_query_metadata_v4 === null || + (can_query_metadata_v4 === undefined && -)} + {can_query_metadata_v4 === true && } + {can_query_metadata_v4 === false && } + + + ); + }, + }, + { + accessorKey: "last_probe.last_updated_utc", + header: "Last Probed At", + Cell: ({ cell }) => { + const parsed = dayjs(cell.getValue()); + return ( + +
+ {parsed.format()} +
+
+ ({parsed.fromNow()}) +
+
+ ); + }, + }, + { + id: "last_probe_age", + accessorKey: "last_probe.last_updated_utc", + header: "Last Probed Age", + Cell: ({ cell, row }) => { + const value = row.original.last_probe?.last_updated_utc; + if (!value) { + return "-"; + } + const parsed = dayjs(value); + const age = dayjs().diff(parsed, "minutes"); + return <>{age} minutes; + }, + }, + { + accessorKey: "build_information.build_version", + header: "Version", + }, + ], + [], + //end + ); + + const table = useMaterialReactTable({ + columns, + data: data || [], + initialState: { + showColumnFilters: true, + density: "compact", + pagination: { + pageIndex: 0, + pageSize: 100, + }, + }, + muiToolbarAlertBannerProps: isError + ? { + color: "error", + children: "Error loading data", + } + : undefined, + renderTopToolbarCustomActions: () => ( + + refetch()}> + + + + ), + rowCount: data?.length || 0, + state: { + isLoading, + showAlertBanner: isError, + showProgressBars: isRefetching, + }, + }); + + return ( + <> + +

Gateway probe age

+ + Average age is {Math.round(staleGateways.average * 10) / 10} minutes old + + + + + + Age + Gateways + + + + {staleGateways.bins.map((r, i) => ( + + + {(i + 1) * staleGatewayBinWidthMinutes} mins old + + {r} + + ))} + +
+
+ + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/dvpn/page.tsx b/nym-node-status-api/nym-node-status-ui/src/app/dvpn/page.tsx new file mode 100644 index 0000000000..e5b5b8fbcf --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/dvpn/page.tsx @@ -0,0 +1,15 @@ +"use client"; + +import GatewaysTable from "@/app/dvpn/GatewaysTable"; +import NestedLayoutWithHeader from "@/layouts/NestedLayoutWithHeader"; +import Box from "@mui/material/Box"; + +export default function Page() { + return ( + + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/favicon.ico b/nym-node-status-api/nym-node-status-ui/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/nym-node-status-api/nym-node-status-ui/src/app/layout.tsx b/nym-node-status-api/nym-node-status-ui/src/app/layout.tsx index 83d23fa86d..7eaf4f838a 100644 --- a/nym-node-status-api/nym-node-status-ui/src/app/layout.tsx +++ b/nym-node-status-api/nym-node-status-ui/src/app/layout.tsx @@ -1,10 +1,30 @@ "use client"; +import { QueryContextProvider } from "@/context/queryContext"; +import LayoutWithNav from "@/layouts/LayoutWithNav"; +import AppTheme from "@/theme"; +import { AppRouterCacheProvider } from "@mui/material-nextjs/v15-appRouter"; +import CssBaseline from "@mui/material/CssBaseline"; +import InitColorSchemeScript from "@mui/material/InitColorSchemeScript"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; + export default function RootLayout(props: { children: React.ReactNode }) { return ( - {props.children} + + + + {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} + + + + {props.children} + + + + ); diff --git a/nym-node-status-api/nym-node-status-ui/src/app/nodes/NodesTable.tsx b/nym-node-status-api/nym-node-status-ui/src/app/nodes/NodesTable.tsx new file mode 100644 index 0000000000..6ee28b2bcf --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/nodes/NodesTable.tsx @@ -0,0 +1,124 @@ +import { useAllNymNodes } from "@/hooks/useAllNymNodes"; +import type { NymNode } from "@/hooks/useNymNodes"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import { IconButton, Tooltip } from "@mui/material"; +import { + type MRT_ColumnDef, + MaterialReactTable, + useMaterialReactTable, +} from "material-react-table"; +import { useMemo } from "react"; + +export default function NodesTable() { + const { data, isError, isRefetching, isLoading, refetch } = + useAllNymNodes().query; + + const columns = useMemo[]>( + //column definitions... + () => [ + { + accessorKey: "node_id", + header: "Node Id", + size: 25, + }, + { + accessorKey: "description.moniker", + header: "Moniker", + }, + { + accessorKey: "identity_key", + header: "Identity Key", + Cell: ({ cell }) => ( + {cell.getValue()?.slice(0, 8)}... + ), + }, + { + accessorKey: "node_type", + header: "Node Type", + }, + { + accessorKey: "bonded", + header: "Bonded", + Cell: ({ cell }) => (cell.getValue() ? "✅" : "⛔️"), + }, + { + accessorKey: "geoip.country", + header: "Country", + }, + { + accessorKey: "geoip.city", + header: "City", + }, + { + accessorKey: "self_description.build_information.build_version", + header: "Version", + }, + { + accessorKey: "self_description.declared_role.entry", + header: "Entry gateway", + Cell: ({ cell }) => (cell.getValue() ? "✅" : "-"), + }, + { + accessorKey: "self_description.declared_role.exit", + header: "Exit gateway", + Cell: ({ cell }) => (cell.getValue() ? "✅" : "-"), + }, + { + accessorKey: "self_description.declared_role.mixnode", + header: "Mixnode", + Cell: ({ cell }) => (cell.getValue() ? "✅" : "-"), + }, + { + accessorKey: "self_description.declared_role.exit_ipr", + header: "Runs IPR", + Cell: ({ cell }) => (cell.getValue() ? "✅" : "-"), + }, + { + accessorKey: "self_description.declared_role.exit_nr", + header: "Runs SOCKS5 NR", + Cell: ({ cell }) => (cell.getValue() ? "✅" : "-"), + }, + { + accessorKey: "self_description.host_information.ip_address", + header: "IP Address", + }, + { + accessorKey: "uptime", + header: "Uptime", + }, + ], + [], + //end + ); + + const table = useMaterialReactTable({ + columns, + data: data || [], + initialState: { + showColumnFilters: true, + density: "compact", + pagination: { pageIndex: 0, pageSize: 100 }, + }, + muiToolbarAlertBannerProps: isError + ? { + color: "error", + children: "Error loading data", + } + : undefined, + renderTopToolbarCustomActions: () => ( + + refetch()}> + + + + ), + rowCount: data?.length ?? 0, + state: { + isLoading, + showAlertBanner: isError, + showProgressBars: isRefetching, + }, + }); + + return ; +} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/nodes/page.tsx b/nym-node-status-api/nym-node-status-ui/src/app/nodes/page.tsx new file mode 100644 index 0000000000..608f32869c --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/nodes/page.tsx @@ -0,0 +1,15 @@ +"use client"; + +import NodesTable from "@/app/nodes/NodesTable"; +import NestedLayoutWithHeader from "@/layouts/NestedLayoutWithHeader"; +import Box from "@mui/material/Box"; + +export default function Page() { + return ( + + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/nym-vpn/GatewaysTable.tsx b/nym-node-status-api/nym-node-status-ui/src/app/nym-vpn/GatewaysTable.tsx new file mode 100644 index 0000000000..6c1de054d0 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/nym-vpn/GatewaysTable.tsx @@ -0,0 +1,115 @@ +import type { DVpnGateway } from "@/client"; +import { useDVpnGateways } from "@/hooks/useGateways"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import { IconButton, Tooltip } from "@mui/material"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; +import relativeTime from "dayjs/plugin/relativeTime"; +import { + type MRT_ColumnDef, + MaterialReactTable, + useMaterialReactTable, +} from "material-react-table"; +import { useMemo } from "react"; +import ReactCountryFlag from "react-country-flag"; + +dayjs.extend(duration); +dayjs.extend(relativeTime); + +const regionNamesInEnglish = new Intl.DisplayNames(["en"], { type: "region" }); + +export default function GatewaysTable() { + const { data, isError, isRefetching, isLoading, refetch } = + useDVpnGateways().query; + + const columns = useMemo[]>( + //column definitions... + () => [ + { + accessorKey: "name", + header: "Name", + }, + { + accessorKey: "identity_key", + header: "Identity Key", + Cell: ({ cell }) => ( + {cell.getValue()?.slice(0, 8)}... + ), + }, + { + accessorKey: "location.two_letter_iso_country_code", + header: "Country", + Cell: ({ cell }) => { + const value = cell.getValue(); + return ( + <> + {value} + + {regionNamesInEnglish.of(value)} + + + ); + }, + }, + { + accessorKey: "last_probe.last_updated_utc", + header: "Last Probed At", + Cell: ({ cell }) => { + const parsed = dayjs(cell.getValue()); + return ( + +
+ {parsed.format()} +
+
+ ({parsed.fromNow()}) +
+
+ ); + }, + }, + { + accessorKey: "build_information.build_version", + header: "Version", + }, + ], + [], + //end + ); + + const table = useMaterialReactTable({ + columns, + data: data || [], + initialState: { + showColumnFilters: true, + density: "compact", + pagination: { + pageIndex: 0, + pageSize: 100, + }, + }, + muiToolbarAlertBannerProps: isError + ? { + color: "error", + children: "Error loading data", + } + : undefined, + renderTopToolbarCustomActions: () => ( + + refetch()}> + + + + ), + rowCount: data?.length || 0, + state: { + isLoading, + showAlertBanner: isError, + showProgressBars: isRefetching, + }, + }); + + return ; +} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/nym-vpn/page.tsx b/nym-node-status-api/nym-node-status-ui/src/app/nym-vpn/page.tsx new file mode 100644 index 0000000000..6f55cf7dba --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/nym-vpn/page.tsx @@ -0,0 +1,15 @@ +"use client"; + +import GatewaysTable from "@/app/dvpn/GatewaysTable"; +import NestedLayoutWithHeader from "@/layouts/NestedLayoutWithHeader"; +import Box from "@mui/material/Box"; + +export default function Page() { + return ( + + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/page.tsx b/nym-node-status-api/nym-node-status-ui/src/app/page.tsx index b77b535926..f3157b0289 100644 --- a/nym-node-status-api/nym-node-status-ui/src/app/page.tsx +++ b/nym-node-status-api/nym-node-status-ui/src/app/page.tsx @@ -1,7 +1,61 @@ "use client"; +import GraphCard from "@/components/GraphCard"; +import { GatewayCanQueryMetadataTopup } from "@/components/graphs/GatewayCanQueryMetadataTopup"; +import { GatewayDownloadSpeeds } from "@/components/graphs/GatewayDownloadSpeeds"; +import { GatewayLoads } from "@/components/graphs/GatewayLoads"; +import { GatewayPingPercentage } from "@/components/graphs/GatewayPingPercentage"; +import { GatewayScores } from "@/components/graphs/GatewayScores"; +import { GatewayUptimePercentage } from "@/components/graphs/GatewayUptimePercentage"; +import { GatewayVersions } from "@/components/graphs/GatewayVersions"; +import NestedLayoutWithHeader from "@/layouts/NestedLayoutWithHeader"; +import Grid from "@mui/material/Grid"; + export default function Home() { return ( -
TODO
+ + theme.spacing(2) }} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); } diff --git a/nym-node-status-api/nym-node-status-ui/src/app/socks5/page.tsx b/nym-node-status-api/nym-node-status-ui/src/app/socks5/page.tsx new file mode 100644 index 0000000000..7cd5172647 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/socks5/page.tsx @@ -0,0 +1,11 @@ +"use client"; + +import NestedLayoutWithHeader from "@/layouts/NestedLayoutWithHeader"; + +export default function Page() { + return ( + +
SOCKS5
+
+ ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/validators/page.tsx b/nym-node-status-api/nym-node-status-ui/src/app/validators/page.tsx new file mode 100644 index 0000000000..6ae3f8edda --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/validators/page.tsx @@ -0,0 +1,11 @@ +"use client"; + +import NestedLayoutWithHeader from "@/layouts/NestedLayoutWithHeader"; + +export default function Page() { + return ( + +
Validators
+
+ ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/zk-nym-signers/page.tsx b/nym-node-status-api/nym-node-status-ui/src/app/zk-nym-signers/page.tsx new file mode 100644 index 0000000000..089eca1d97 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/zk-nym-signers/page.tsx @@ -0,0 +1,11 @@ +"use client"; + +import NestedLayoutWithHeader from "@/layouts/NestedLayoutWithHeader"; + +export default function Page() { + return ( + +
zk-nym signers
+
+ ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/client/@tanstack/react-query.gen.ts b/nym-node-status-api/nym-node-status-ui/src/client/@tanstack/react-query.gen.ts new file mode 100644 index 0000000000..5e672b9bb7 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/@tanstack/react-query.gen.ts @@ -0,0 +1,936 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { + type DefaultError, + type InfiniteData, + infiniteQueryOptions, + queryOptions, +} from "@tanstack/react-query"; +import { client as _heyApiClient } from "../client.gen"; +import { + type Options, + buildInformation, + gateways, + gatewaysSkinny, + getAllSessions, + getEntryGatewayCountries, + getEntryGateways, + getEntryGatewaysByCountry, + getExitGatewayCountries, + getExitGateways, + getExitGatewaysByCountry, + getGateway, + getGatewayCountries, + getGateways, + getGatewaysByCountry, + getMixnodes, + getStats, + health, + mixnodes, + mixnodes2, + nodeDelegations, + nymNodes, + summary, + summaryHistory, +} from "../sdk.gen"; +import type { + BuildInformationData, + GatewaysData, + GatewaysResponse, + GatewaysSkinnyData, + GatewaysSkinnyResponse, + GetAllSessionsData, + GetAllSessionsResponse, + GetEntryGatewayCountriesData, + GetEntryGatewaysByCountryData, + GetEntryGatewaysData, + GetExitGatewayCountriesData, + GetExitGatewaysByCountryData, + GetExitGatewaysData, + GetGatewayCountriesData, + GetGatewayData, + GetGatewaysByCountryData, + GetGatewaysData, + GetMixnodesData, + GetStatsData, + GetStatsResponse, + HealthData, + Mixnodes2Data, + Mixnodes2Response, + MixnodesData, + MixnodesResponse, + NodeDelegationsData, + NymNodesData, + NymNodesResponse, + SummaryData, + SummaryHistoryData, +} from "../types.gen"; + +export type QueryKey = [ + Pick & { + _id: string; + _infinite?: boolean; + }, +]; + +const createQueryKey = ( + id: string, + options?: TOptions, + infinite?: boolean, +): [QueryKey[0]] => { + const params: QueryKey[0] = { + _id: id, + baseUrl: (options?.client ?? _heyApiClient).getConfig().baseUrl, + } as QueryKey[0]; + if (infinite) { + params._infinite = infinite; + } + if (options?.body) { + params.body = options.body; + } + if (options?.headers) { + params.headers = options.headers; + } + if (options?.path) { + params.path = options.path; + } + if (options?.query) { + params.query = options.query; + } + return [params]; +}; + +export const getGatewaysQueryKey = (options?: Options) => + createQueryKey("getGateways", options); + +/** + * Gets available entry and exit gateways from the Nym network directory + */ +export const getGatewaysOptions = (options?: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getGateways({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getGatewaysQueryKey(options), + }); +}; + +export const getGatewayCountriesQueryKey = ( + options?: Options, +) => createQueryKey("getGatewayCountries", options); + +/** + * Gets available exit gateway countries as two-letter ISO country codes from the Nym network directory + */ +export const getGatewayCountriesOptions = ( + options?: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getGatewayCountries({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getGatewayCountriesQueryKey(options), + }); +}; + +export const getGatewaysByCountryQueryKey = ( + options: Options, +) => createQueryKey("getGatewaysByCountry", options); + +/** + * Gets available gateways from the Nym network directory by country + */ +export const getGatewaysByCountryOptions = ( + options: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getGatewaysByCountry({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getGatewaysByCountryQueryKey(options), + }); +}; + +export const getEntryGatewaysQueryKey = ( + options?: Options, +) => createQueryKey("getEntryGateways", options); + +/** + * Gets available entry gateways from the Nym network directory + */ +export const getEntryGatewaysOptions = ( + options?: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getEntryGateways({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getEntryGatewaysQueryKey(options), + }); +}; + +export const getEntryGatewayCountriesQueryKey = ( + options?: Options, +) => createQueryKey("getEntryGatewayCountries", options); + +/** + * Gets available entry gateway countries as two-letter ISO country codes from the Nym network directory + */ +export const getEntryGatewayCountriesOptions = ( + options?: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getEntryGatewayCountries({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getEntryGatewayCountriesQueryKey(options), + }); +}; + +export const getEntryGatewaysByCountryQueryKey = ( + options: Options, +) => createQueryKey("getEntryGatewaysByCountry", options); + +/** + * Gets available entry gateways from the Nym network directory by country + */ +export const getEntryGatewaysByCountryOptions = ( + options: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getEntryGatewaysByCountry({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getEntryGatewaysByCountryQueryKey(options), + }); +}; + +export const getExitGatewaysQueryKey = ( + options?: Options, +) => createQueryKey("getExitGateways", options); + +/** + * Gets available exit gateways from the Nym network directory + */ +export const getExitGatewaysOptions = ( + options?: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getExitGateways({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getExitGatewaysQueryKey(options), + }); +}; + +export const getExitGatewayCountriesQueryKey = ( + options?: Options, +) => createQueryKey("getExitGatewayCountries", options); + +/** + * Gets available exit gateway countries as two-letter ISO country codes from the Nym network directory + */ +export const getExitGatewayCountriesOptions = ( + options?: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getExitGatewayCountries({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getExitGatewayCountriesQueryKey(options), + }); +}; + +export const getExitGatewaysByCountryQueryKey = ( + options: Options, +) => createQueryKey("getExitGatewaysByCountry", options); + +/** + * Gets available exit gateways from the Nym network directory by country + */ +export const getExitGatewaysByCountryOptions = ( + options: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getExitGatewaysByCountry({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getExitGatewaysByCountryQueryKey(options), + }); +}; + +export const nymNodesQueryKey = (options?: Options) => + createQueryKey("nymNodes", options); + +export const nymNodesOptions = (options?: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await nymNodes({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: nymNodesQueryKey(options), + }); +}; + +const createInfiniteParams = < + K extends Pick[0], "body" | "headers" | "path" | "query">, +>( + queryKey: QueryKey, + page: K, +) => { + const params = { + ...queryKey[0], + }; + if (page.body) { + params.body = { + ...(queryKey[0].body as any), + ...(page.body as any), + }; + } + if (page.headers) { + params.headers = { + ...queryKey[0].headers, + ...page.headers, + }; + } + if (page.path) { + params.path = { + ...(queryKey[0].path as any), + ...(page.path as any), + }; + } + if (page.query) { + params.query = { + ...(queryKey[0].query as any), + ...(page.query as any), + }; + } + return params as unknown as typeof page; +}; + +export const nymNodesInfiniteQueryKey = ( + options?: Options, +): QueryKey> => createQueryKey("nymNodes", options, true); + +export const nymNodesInfiniteOptions = (options?: Options) => { + return infiniteQueryOptions< + NymNodesResponse, + DefaultError, + InfiniteData, + QueryKey>, + | number + | Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > + >( + // @ts-ignore + { + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > = + typeof pageParam === "object" + ? pageParam + : { + query: { + page: pageParam, + }, + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await nymNodes({ + ...options, + ...params, + signal, + throwOnError: true, + }); + return data; + }, + queryKey: nymNodesInfiniteQueryKey(options), + }, + ); +}; + +export const nodeDelegationsQueryKey = ( + options: Options, +) => createQueryKey("nodeDelegations", options); + +export const nodeDelegationsOptions = ( + options: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await nodeDelegations({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: nodeDelegationsQueryKey(options), + }); +}; + +export const gatewaysQueryKey = (options?: Options) => + createQueryKey("gateways", options); + +export const gatewaysOptions = (options?: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await gateways({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: gatewaysQueryKey(options), + }); +}; + +export const gatewaysInfiniteQueryKey = ( + options?: Options, +): QueryKey> => createQueryKey("gateways", options, true); + +export const gatewaysInfiniteOptions = (options?: Options) => { + return infiniteQueryOptions< + GatewaysResponse, + DefaultError, + InfiniteData, + QueryKey>, + | number + | Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > + >( + // @ts-ignore + { + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > = + typeof pageParam === "object" + ? pageParam + : { + query: { + page: pageParam, + }, + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await gateways({ + ...options, + ...params, + signal, + throwOnError: true, + }); + return data; + }, + queryKey: gatewaysInfiniteQueryKey(options), + }, + ); +}; + +export const gatewaysSkinnyQueryKey = (options?: Options) => + createQueryKey("gatewaysSkinny", options); + +export const gatewaysSkinnyOptions = ( + options?: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await gatewaysSkinny({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: gatewaysSkinnyQueryKey(options), + }); +}; + +export const gatewaysSkinnyInfiniteQueryKey = ( + options?: Options, +): QueryKey> => + createQueryKey("gatewaysSkinny", options, true); + +export const gatewaysSkinnyInfiniteOptions = ( + options?: Options, +) => { + return infiniteQueryOptions< + GatewaysSkinnyResponse, + DefaultError, + InfiniteData, + QueryKey>, + | number + | Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > + >( + // @ts-ignore + { + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > = + typeof pageParam === "object" + ? pageParam + : { + query: { + page: pageParam, + }, + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await gatewaysSkinny({ + ...options, + ...params, + signal, + throwOnError: true, + }); + return data; + }, + queryKey: gatewaysSkinnyInfiniteQueryKey(options), + }, + ); +}; + +export const getGatewayQueryKey = (options: Options) => + createQueryKey("getGateway", options); + +export const getGatewayOptions = (options: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getGateway({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getGatewayQueryKey(options), + }); +}; + +export const getAllSessionsQueryKey = (options?: Options) => + createQueryKey("getAllSessions", options); + +export const getAllSessionsOptions = ( + options?: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getAllSessions({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getAllSessionsQueryKey(options), + }); +}; + +export const getAllSessionsInfiniteQueryKey = ( + options?: Options, +): QueryKey> => + createQueryKey("getAllSessions", options, true); + +export const getAllSessionsInfiniteOptions = ( + options?: Options, +) => { + return infiniteQueryOptions< + GetAllSessionsResponse, + DefaultError, + InfiniteData, + QueryKey>, + | number + | Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > + >( + // @ts-ignore + { + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > = + typeof pageParam === "object" + ? pageParam + : { + query: { + page: pageParam, + }, + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await getAllSessions({ + ...options, + ...params, + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getAllSessionsInfiniteQueryKey(options), + }, + ); +}; + +export const mixnodesQueryKey = (options?: Options) => + createQueryKey("mixnodes", options); + +export const mixnodesOptions = (options?: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await mixnodes({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: mixnodesQueryKey(options), + }); +}; + +export const mixnodesInfiniteQueryKey = ( + options?: Options, +): QueryKey> => createQueryKey("mixnodes", options, true); + +export const mixnodesInfiniteOptions = (options?: Options) => { + return infiniteQueryOptions< + MixnodesResponse, + DefaultError, + InfiniteData, + QueryKey>, + | number + | Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > + >( + // @ts-ignore + { + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > = + typeof pageParam === "object" + ? pageParam + : { + query: { + page: pageParam, + }, + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await mixnodes({ + ...options, + ...params, + signal, + throwOnError: true, + }); + return data; + }, + queryKey: mixnodesInfiniteQueryKey(options), + }, + ); +}; + +export const getStatsQueryKey = (options?: Options) => + createQueryKey("getStats", options); + +export const getStatsOptions = (options?: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getStats({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getStatsQueryKey(options), + }); +}; + +export const getStatsInfiniteQueryKey = ( + options?: Options, +): QueryKey> => createQueryKey("getStats", options, true); + +export const getStatsInfiniteOptions = (options?: Options) => { + return infiniteQueryOptions< + GetStatsResponse, + DefaultError, + InfiniteData, + QueryKey>, + | number + | Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > + >( + // @ts-ignore + { + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > = + typeof pageParam === "object" + ? pageParam + : { + query: { + offset: pageParam, + }, + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await getStats({ + ...options, + ...params, + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getStatsInfiniteQueryKey(options), + }, + ); +}; + +export const getMixnodesQueryKey = (options: Options) => + createQueryKey("getMixnodes", options); + +export const getMixnodesOptions = (options: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getMixnodes({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getMixnodesQueryKey(options), + }); +}; + +export const mixnodes2QueryKey = (options?: Options) => + createQueryKey("mixnodes2", options); + +export const mixnodes2Options = (options?: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await mixnodes2({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: mixnodes2QueryKey(options), + }); +}; + +export const mixnodes2InfiniteQueryKey = ( + options?: Options, +): QueryKey> => + createQueryKey("mixnodes2", options, true); + +export const mixnodes2InfiniteOptions = (options?: Options) => { + return infiniteQueryOptions< + Mixnodes2Response, + DefaultError, + InfiniteData, + QueryKey>, + | number + | Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > + >( + // @ts-ignore + { + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > = + typeof pageParam === "object" + ? pageParam + : { + query: { + page: pageParam, + }, + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await mixnodes2({ + ...options, + ...params, + signal, + throwOnError: true, + }); + return data; + }, + queryKey: mixnodes2InfiniteQueryKey(options), + }, + ); +}; + +export const buildInformationQueryKey = ( + options?: Options, +) => createQueryKey("buildInformation", options); + +export const buildInformationOptions = ( + options?: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await buildInformation({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: buildInformationQueryKey(options), + }); +}; + +export const healthQueryKey = (options?: Options) => + createQueryKey("health", options); + +export const healthOptions = (options?: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await health({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: healthQueryKey(options), + }); +}; + +export const summaryQueryKey = (options?: Options) => + createQueryKey("summary", options); + +export const summaryOptions = (options?: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await summary({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: summaryQueryKey(options), + }); +}; + +export const summaryHistoryQueryKey = (options?: Options) => + createQueryKey("summaryHistory", options); + +export const summaryHistoryOptions = ( + options?: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await summaryHistory({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: summaryHistoryQueryKey(options), + }); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/client.gen.ts b/nym-node-status-api/nym-node-status-ui/src/client/client.gen.ts new file mode 100644 index 0000000000..d4af1d498e --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/client.gen.ts @@ -0,0 +1,28 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { + type Config, + type ClientOptions as DefaultClientOptions, + createClient, + createConfig, +} from "./client"; +import type { ClientOptions } from "./types.gen"; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = + ( + override?: Config, + ) => Config & T>; + +export const client = createClient( + createConfig({ + baseUrl: "https://mainnet-node-status-api.nymtech.cc", + }), +); diff --git a/nym-node-status-api/nym-node-status-ui/src/client/client/client.ts b/nym-node-status-api/nym-node-status-ui/src/client/client/client.ts new file mode 100644 index 0000000000..748558f584 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/client/client.ts @@ -0,0 +1,195 @@ +import type { Client, Config, RequestOptions } from "./types"; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from "./utils"; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + Request, + Response, + unknown, + RequestOptions + >(); + + const request: Client["request"] = async (options) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body && opts.bodySerializer) { + opts.body = opts.bodySerializer(opts.body); + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.body === "") { + opts.headers.delete("Content-Type"); + } + + const url = buildUrl(opts); + const requestInit: ReqInit = { + redirect: "follow", + ...opts, + }; + + let request = new Request(url, requestInit); + + for (const fn of interceptors.request._fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + let response = await _fetch(request); + + for (const fn of interceptors.response._fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + if ( + response.status === 204 || + response.headers.get("Content-Length") === "0" + ) { + return opts.responseStyle === "data" + ? {} + : { + data: {}, + ...result, + }; + } + + const parseAs = + (opts.parseAs === "auto" + ? getParseAs(response.headers.get("Content-Type")) + : opts.parseAs) ?? "json"; + + let data: any; + switch (parseAs) { + case "arrayBuffer": + case "blob": + case "formData": + case "json": + case "text": + data = await response[parseAs](); + break; + case "stream": + return opts.responseStyle === "data" + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === "json") { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === "data" + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + const error = jsonError ?? textError; + let finalError = error; + + for (const fn of interceptors.error._fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string; + } + } + + finalError = finalError || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return opts.responseStyle === "data" + ? undefined + : { + error: finalError, + ...result, + }; + }; + + return { + buildUrl, + connect: (options) => request({ ...options, method: "CONNECT" }), + delete: (options) => request({ ...options, method: "DELETE" }), + get: (options) => request({ ...options, method: "GET" }), + getConfig, + head: (options) => request({ ...options, method: "HEAD" }), + interceptors, + options: (options) => request({ ...options, method: "OPTIONS" }), + patch: (options) => request({ ...options, method: "PATCH" }), + post: (options) => request({ ...options, method: "POST" }), + put: (options) => request({ ...options, method: "PUT" }), + request, + setConfig, + trace: (options) => request({ ...options, method: "TRACE" }), + }; +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/client/index.ts b/nym-node-status-api/nym-node-status-ui/src/client/client/index.ts new file mode 100644 index 0000000000..7b4add4180 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/client/index.ts @@ -0,0 +1,22 @@ +export type { Auth } from "../core/auth"; +export type { QuerySerializerOptions } from "../core/bodySerializer"; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from "../core/bodySerializer"; +export { buildClientParams } from "../core/params"; +export { createClient } from "./client"; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + OptionsLegacyParser, + RequestOptions, + RequestResult, + ResponseStyle, + TDataShape, +} from "./types"; +export { createConfig, mergeHeaders } from "./utils"; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/client/types.ts b/nym-node-status-api/nym-node-status-ui/src/client/client/types.ts new file mode 100644 index 0000000000..5344ff3d2a --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/client/types.ts @@ -0,0 +1,219 @@ +import type { Auth } from "../core/auth"; +import type { Client as CoreClient, Config as CoreConfig } from "../core/types"; +import type { Middleware } from "./utils"; + +export type ResponseStyle = "data" | "fields"; + +export interface Config + extends Omit, + CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T["baseUrl"]; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: (request: Request) => ReturnType; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: + | "arrayBuffer" + | "auto" + | "blob" + | "formData" + | "json" + | "stream" + | "text"; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T["throwOnError"]; +} + +export interface RequestOptions< + TResponseStyle extends ResponseStyle = "fields", + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }> { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = "fields", +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends "data" + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record + ? TData[keyof TData] + : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends "data" + ? + | (TData extends Record + ? TData[keyof TData] + : TData) + | undefined + : ( + | { + data: TData extends Record + ? TData[keyof TData] + : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record + ? TError[keyof TError] + : TError; + } + ) & { + request: Request; + response: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = "fields", +>( + options: Omit, "method">, +) => RequestResult; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = "fields", +>( + options: Omit, "method"> & + Pick>, "method">, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: Pick & Options, +) => string; + +export type Client = CoreClient & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = "fields", +> = OmitKeys< + RequestOptions, + "body" | "path" | "query" | "url" +> & + Omit; + +export type OptionsLegacyParser< + TData = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = "fields", +> = TData extends { body?: any } + ? TData extends { headers?: any } + ? OmitKeys< + RequestOptions, + "body" | "headers" | "url" + > & + TData + : OmitKeys, "body" | "url"> & + TData & + Pick, "headers"> + : TData extends { headers?: any } + ? OmitKeys< + RequestOptions, + "headers" | "url" + > & + TData & + Pick, "body"> + : OmitKeys, "url"> & TData; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/client/utils.ts b/nym-node-status-api/nym-node-status-ui/src/client/client/utils.ts new file mode 100644 index 0000000000..ff8112bd56 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/client/utils.ts @@ -0,0 +1,417 @@ +import { getAuthToken } from "../core/auth"; +import type { + QuerySerializer, + QuerySerializerOptions, +} from "../core/bodySerializer"; +import { jsonBodySerializer } from "../core/bodySerializer"; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from "../core/pathSerializer"; +import type { Client, ClientOptions, Config, RequestOptions } from "./types"; + +interface PathSerializer { + path: Record; + url: string; +} + +const PATH_PARAM_RE = /\{[^{}]+\}/g; + +type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited"; +type MatrixStyle = "label" | "matrix" | "simple"; +type ArraySeparatorStyle = ArrayStyle | MatrixStyle; + +const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = "simple"; + + if (name.endsWith("*")) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith(".")) { + name = name.substring(1); + style = "label"; + } else if (name.startsWith(";")) { + name = name.substring(1); + style = "matrix"; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === "object") { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === "matrix") { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === "label" ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const createQuerySerializer = ({ + allowReserved, + array, + object, +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === "object") { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved, + explode: true, + name, + style: "form", + value, + ...array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === "object") { + const serializedObject = serializeObjectParam({ + allowReserved, + explode: true, + name, + style: "deepObject", + value: value as Record, + ...object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join("&"); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = ( + contentType: string | null, +): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return "stream"; + } + + const cleanContent = contentType.split(";")[0]?.trim(); + + if (!cleanContent) { + return; + } + + if ( + cleanContent.startsWith("application/json") || + cleanContent.endsWith("+json") + ) { + return "json"; + } + + if (cleanContent === "multipart/form-data") { + return "formData"; + } + + if ( + ["application/", "audio/", "image/", "video/"].some((type) => + cleanContent.startsWith(type), + ) + ) { + return "blob"; + } + + if (cleanContent.startsWith("text/")) { + return "text"; + } + + return; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, "security"> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? "Authorization"; + + switch (auth.in) { + case "query": + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case "cookie": + options.headers.append("Cookie", `${name}=${token}`); + break; + case "header": + default: + options.headers.set(name, token); + break; + } + + return; + } +}; + +export const buildUrl: Client["buildUrl"] = (options) => { + const url = getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === "function" + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith("/") ? _url : `/${_url}`; + let url = (baseUrl ?? "") + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ""; + if (search.startsWith("?")) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith("/")) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +export const mergeHeaders = ( + ...headers: Array["headers"] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header || typeof header !== "object") { + continue; + } + + const iterator = + header instanceof Headers ? header.entries() : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === "object" ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + response: Res, + request: Req, + options: Options, +) => Err | Promise; + +type ReqInterceptor = ( + request: Req, + options: Options, +) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + _fns: (Interceptor | null)[]; + + constructor() { + this._fns = []; + } + + clear() { + this._fns = []; + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === "number") { + return this._fns[id] ? id : -1; + } else { + return this._fns.indexOf(id); + } + } + exists(id: number | Interceptor) { + const index = this.getInterceptorIndex(id); + return !!this._fns[index]; + } + + eject(id: number | Interceptor) { + const index = this.getInterceptorIndex(id); + if (this._fns[index]) { + this._fns[index] = null; + } + } + + update(id: number | Interceptor, fn: Interceptor) { + const index = this.getInterceptorIndex(id); + if (this._fns[index]) { + this._fns[index] = fn; + return id; + } else { + return false; + } + } + + use(fn: Interceptor) { + this._fns = [...this._fns, fn]; + return this._fns.length - 1; + } +} + +// `createInterceptors()` response, meant for external use as it does not +// expose internals +export interface Middleware { + error: Pick< + Interceptors>, + "eject" | "use" + >; + request: Pick>, "eject" | "use">; + response: Pick< + Interceptors>, + "eject" | "use" + >; +} + +// do not add `Middleware` as return type so we can use _fns internally +export const createInterceptors = () => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: "form", + }, + object: { + explode: true, + style: "deepObject", + }, +}); + +const defaultHeaders = { + "Content-Type": "application/json", +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: "auto", + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/nym-node-status-api/nym-node-status-ui/src/client/core/auth.ts b/nym-node-status-api/nym-node-status-ui/src/client/core/auth.ts new file mode 100644 index 0000000000..70e9d5dba5 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/core/auth.ts @@ -0,0 +1,40 @@ +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: "header" | "query" | "cookie"; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: "basic" | "bearer"; + type: "apiKey" | "http"; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = + typeof callback === "function" ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === "bearer") { + return `Bearer ${token}`; + } + + if (auth.scheme === "basic") { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/core/bodySerializer.ts b/nym-node-status-api/nym-node-status-ui/src/client/core/bodySerializer.ts new file mode 100644 index 0000000000..35e2a17e1a --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/core/bodySerializer.ts @@ -0,0 +1,88 @@ +import type { + ArrayStyle, + ObjectStyle, + SerializerOptions, +} from "./pathSerializer"; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: any) => any; + +export interface QuerySerializerOptions { + allowReserved?: boolean; + array?: SerializerOptions; + object?: SerializerOptions; +} + +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { + if (typeof value === "string" || value instanceof Blob) { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = ( + data: URLSearchParams, + key: string, + value: unknown, +): void => { + if (typeof value === "string") { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): FormData => { + const data = new FormData(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => + typeof value === "bigint" ? value.toString() : value, + ), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): string => { + const data = new URLSearchParams(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/core/params.ts b/nym-node-status-api/nym-node-status-ui/src/client/core/params.ts new file mode 100644 index 0000000000..0771542b14 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/core/params.ts @@ -0,0 +1,141 @@ +type Slot = "body" | "headers" | "path" | "query"; + +export type Field = + | { + in: Exclude; + key: string; + map?: string; + } + | { + in: Extract; + key?: string; + map?: string; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: "body", + $headers_: "headers", + $path_: "path", + $query_: "query", +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + { + in: Slot; + map?: string; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ("in" in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === "object" && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = ( + args: ReadonlyArray, + fields: FieldsConfig, +) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ("in" in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + (params[field.in] as Record)[name] = arg; + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + const extra = extraPrefixes.find(([prefix]) => + key.startsWith(prefix), + ); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[ + key.slice(prefix.length) + ] = value; + } else { + for (const [slot, allowed] of Object.entries( + config.allowExtra ?? {}, + )) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/core/pathSerializer.ts b/nym-node-status-api/nym-node-status-ui/src/client/core/pathSerializer.ts new file mode 100644 index 0000000000..4052ad1279 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/core/pathSerializer.ts @@ -0,0 +1,179 @@ +interface SerializeOptions + extends SerializePrimitiveOptions, + SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited"; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = "label" | "matrix" | "simple"; +export type ObjectStyle = "form" | "deepObject"; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case "label": + return "."; + case "matrix": + return ";"; + case "simple": + return ","; + default: + return "&"; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case "form": + return ","; + case "pipeDelimited": + return "|"; + case "spaceDelimited": + return "%20"; + default: + return ","; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case "label": + return "."; + case "matrix": + return ";"; + case "simple": + return ","; + default: + return "&"; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case "label": + return `.${joinedValues}`; + case "matrix": + return `;${name}=${joinedValues}`; + case "simple": + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === "label" || style === "simple") { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === "label" || style === "matrix" + ? separator + joinedValues + : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ""; + } + + if (typeof value === "object") { + throw new Error( + "Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.", + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== "deepObject" && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [ + ...values, + key, + allowReserved ? (v as string) : encodeURIComponent(v as string), + ]; + }); + const joinedValues = values.join(","); + switch (style) { + case "form": + return `${name}=${joinedValues}`; + case "label": + return `.${joinedValues}`; + case "matrix": + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === "deepObject" ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === "label" || style === "matrix" + ? separator + joinedValues + : joinedValues; +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/core/types.ts b/nym-node-status-api/nym-node-status-ui/src/client/core/types.ts new file mode 100644 index 0000000000..940f22a77f --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/core/types.ts @@ -0,0 +1,104 @@ +import type { Auth, AuthToken } from "./auth"; +import type { + BodySerializer, + QuerySerializer, + QuerySerializerOptions, +} from "./bodySerializer"; + +export interface Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, +> { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + connect: MethodFn; + delete: MethodFn; + get: MethodFn; + getConfig: () => Config; + head: MethodFn; + options: MethodFn; + patch: MethodFn; + post: MethodFn; + put: MethodFn; + request: RequestFn; + setConfig: (config: Config) => Config; + trace: MethodFn; +} + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit["headers"] + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: + | "CONNECT" + | "DELETE" + | "GET" + | "HEAD" + | "OPTIONS" + | "PATCH" + | "POST" + | "PUT" + | "TRACE"; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g. converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} diff --git a/nym-node-status-api/nym-node-status-ui/src/client/index.ts b/nym-node-status-api/nym-node-status-ui/src/client/index.ts new file mode 100644 index 0000000000..da87079367 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/index.ts @@ -0,0 +1,3 @@ +// This file is auto-generated by @hey-api/openapi-ts +export * from "./types.gen"; +export * from "./sdk.gen"; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/sdk.gen.ts b/nym-node-status-api/nym-node-status-ui/src/client/sdk.gen.ts new file mode 100644 index 0000000000..b571278744 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/sdk.gen.ts @@ -0,0 +1,395 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Client, Options as ClientOptions, TDataShape } from "./client"; +import { client as _heyApiClient } from "./client.gen"; +import type { + BuildInformationData, + BuildInformationResponses, + GatewaysData, + GatewaysResponses, + GatewaysSkinnyData, + GatewaysSkinnyResponses, + GetAllSessionsData, + GetAllSessionsResponses, + GetEntryGatewayCountriesData, + GetEntryGatewayCountriesResponses, + GetEntryGatewaysByCountryData, + GetEntryGatewaysByCountryResponses, + GetEntryGatewaysData, + GetEntryGatewaysResponses, + GetExitGatewayCountriesData, + GetExitGatewayCountriesResponses, + GetExitGatewaysByCountryData, + GetExitGatewaysByCountryResponses, + GetExitGatewaysData, + GetExitGatewaysResponses, + GetGatewayCountriesData, + GetGatewayCountriesResponses, + GetGatewayData, + GetGatewayResponses, + GetGatewaysByCountryData, + GetGatewaysByCountryResponses, + GetGatewaysData, + GetGatewaysResponses, + GetMixnodesData, + GetMixnodesResponses, + GetStatsData, + GetStatsResponses, + HealthData, + HealthResponses, + Mixnodes2Data, + Mixnodes2Responses, + MixnodesData, + MixnodesResponses, + NodeDelegationsData, + NodeDelegationsResponses, + NymNodesData, + NymNodesResponses, + SummaryData, + SummaryHistoryData, + SummaryHistoryResponses, + SummaryResponses, +} from "./types.gen"; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, +> = ClientOptions & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +/** + * Gets available entry and exit gateways from the Nym network directory + */ +export const getGateways = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GetGatewaysResponses, + unknown, + ThrowOnError + >({ + url: "/dvpn/v1/directory/gateways", + ...options, + }); +}; + +/** + * Gets available exit gateway countries as two-letter ISO country codes from the Nym network directory + */ +export const getGatewayCountries = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GetGatewayCountriesResponses, + unknown, + ThrowOnError + >({ + url: "/dvpn/v1/directory/gateways/countries", + ...options, + }); +}; + +/** + * Gets available gateways from the Nym network directory by country + */ +export const getGatewaysByCountry = ( + options: Options, +) => { + return (options.client ?? _heyApiClient).get< + GetGatewaysByCountryResponses, + unknown, + ThrowOnError + >({ + url: "/dvpn/v1/directory/gateways/country/{two_letter_country_code}", + ...options, + }); +}; + +/** + * Gets available entry gateways from the Nym network directory + */ +export const getEntryGateways = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GetEntryGatewaysResponses, + unknown, + ThrowOnError + >({ + url: "/dvpn/v1/directory/gateways/entry", + ...options, + }); +}; + +/** + * Gets available entry gateway countries as two-letter ISO country codes from the Nym network directory + */ +export const getEntryGatewayCountries = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GetEntryGatewayCountriesResponses, + unknown, + ThrowOnError + >({ + url: "/dvpn/v1/directory/gateways/entry/countries", + ...options, + }); +}; + +/** + * Gets available entry gateways from the Nym network directory by country + */ +export const getEntryGatewaysByCountry = ( + options: Options, +) => { + return (options.client ?? _heyApiClient).get< + GetEntryGatewaysByCountryResponses, + unknown, + ThrowOnError + >({ + url: "/dvpn/v1/directory/gateways/entry/country/{two_letter_country_code}", + ...options, + }); +}; + +/** + * Gets available exit gateways from the Nym network directory + */ +export const getExitGateways = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GetExitGatewaysResponses, + unknown, + ThrowOnError + >({ + url: "/dvpn/v1/directory/gateways/exit", + ...options, + }); +}; + +/** + * Gets available exit gateway countries as two-letter ISO country codes from the Nym network directory + */ +export const getExitGatewayCountries = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GetExitGatewayCountriesResponses, + unknown, + ThrowOnError + >({ + url: "/dvpn/v1/directory/gateways/exit/countries", + ...options, + }); +}; + +/** + * Gets available exit gateways from the Nym network directory by country + */ +export const getExitGatewaysByCountry = ( + options: Options, +) => { + return (options.client ?? _heyApiClient).get< + GetExitGatewaysByCountryResponses, + unknown, + ThrowOnError + >({ + url: "/dvpn/v1/directory/gateways/exit/country/{two_letter_country_code}", + ...options, + }); +}; + +export const nymNodes = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + NymNodesResponses, + unknown, + ThrowOnError + >({ + url: "/explorer/v3/nym-nodes", + ...options, + }); +}; + +export const nodeDelegations = ( + options: Options, +) => { + return (options.client ?? _heyApiClient).get< + NodeDelegationsResponses, + unknown, + ThrowOnError + >({ + url: "/explorer/v3/nym-nodes/{node_id}/delegations", + ...options, + }); +}; + +export const gateways = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GatewaysResponses, + unknown, + ThrowOnError + >({ + url: "/v2/gateways", + ...options, + }); +}; + +export const gatewaysSkinny = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GatewaysSkinnyResponses, + unknown, + ThrowOnError + >({ + url: "/v2/gateways/skinny", + ...options, + }); +}; + +export const getGateway = ( + options: Options, +) => { + return (options.client ?? _heyApiClient).get< + GetGatewayResponses, + unknown, + ThrowOnError + >({ + url: "/v2/gateways/{identity_key}", + ...options, + }); +}; + +export const getAllSessions = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GetAllSessionsResponses, + unknown, + ThrowOnError + >({ + url: "/v2/metrics/sessions", + ...options, + }); +}; + +export const mixnodes = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + MixnodesResponses, + unknown, + ThrowOnError + >({ + url: "/v2/mixnodes", + ...options, + }); +}; + +export const getStats = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GetStatsResponses, + unknown, + ThrowOnError + >({ + url: "/v2/mixnodes/stats", + ...options, + }); +}; + +export const getMixnodes = ( + options: Options, +) => { + return (options.client ?? _heyApiClient).get< + GetMixnodesResponses, + unknown, + ThrowOnError + >({ + url: "/v2/mixnodes/{mix_id}", + ...options, + }); +}; + +export const mixnodes2 = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + Mixnodes2Responses, + unknown, + ThrowOnError + >({ + url: "/v2/services", + ...options, + }); +}; + +export const buildInformation = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + BuildInformationResponses, + unknown, + ThrowOnError + >({ + url: "/v2/status/build_information", + ...options, + }); +}; + +export const health = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + HealthResponses, + unknown, + ThrowOnError + >({ + url: "/v2/status/health", + ...options, + }); +}; + +export const summary = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + SummaryResponses, + unknown, + ThrowOnError + >({ + url: "/v2/summary", + ...options, + }); +}; + +export const summaryHistory = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + SummaryHistoryResponses, + unknown, + ThrowOnError + >({ + url: "/v2/summary/history", + ...options, + }); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/types.gen.ts b/nym-node-status-api/nym-node-status-ui/src/client/types.gen.ts new file mode 100644 index 0000000000..bb6b7f5839 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/types.gen.ts @@ -0,0 +1,940 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AnnouncePorts = { + mix_port?: number | null; + verloc_port?: number | null; +}; + +export type AuthenticatorDetails = { + /** + * address of the embedded authenticator + */ + address: string; +}; + +/** + * Auxiliary details of the associated Nym Node. + */ +export type AuxiliaryDetails = { + /** + * Specifies whether this node operator has agreed to the terms and conditions + * as defined at + */ + accepted_operator_terms_and_conditions?: boolean; + announce_ports?: AnnouncePorts; + /** + * Optional ISO 3166 alpha-2 two-letter country code of the node's **physical** location + */ + location?: string | null; +}; + +export type BasicEntryInformation = { + hostname?: string | null; + ws_port: number; + wss_port?: number | null; +}; + +export type BinaryBuildInformationOwned = { + /** + * Provides the name of the binary, i.e. the content of `CARGO_PKG_NAME` environmental variable. + */ + binary_name: string; + /** + * Provides the build timestamp, for example `2021-02-23T20:14:46.558472672+00:00`. + */ + build_timestamp: string; + /** + * Provides the build version, for example `0.1.0-9-g46f83e1`. + */ + build_version: string; + /** + * Provides the cargo debug mode that was used for the build. + */ + cargo_profile: string; + /** + * Provides the cargo target triple that was used for the build. + */ + cargo_triple?: string; + /** + * Provides the name of the git branch that was used for the build, for example `master`. + */ + commit_branch: string; + /** + * Provides the hash of the commit that was used for the build, for example `46f83e112520533338245862d366f6a02cef07d4`. + */ + commit_sha: string; + /** + * Provides the timestamp of the commit that was used for the build, for example `2021-02-23T08:08:02-05:00`. + */ + commit_timestamp: string; + /** + * Provides the rustc channel that was used for the build, for example `nightly`. + */ + rustc_channel: string; + /** + * Provides the rustc version that was used for the build, for example `1.52.0-nightly`. + */ + rustc_version: string; +}; + +export type BuildInformation = { + build_version: string; + commit_branch: string; + commit_sha: string; +}; + +/** + * Coin + */ +export type CoinSchema = { + amount: string; + denom: string; +}; + +export type DVpnGateway = { + authenticator?: null | AuthenticatorDetails; + build_information: BinaryBuildInformationOwned; + entry?: null | BasicEntryInformation; + identity_key: string; + ip_addresses: Array; + ip_packet_router?: null | IpPacketRouterDetails; + last_probe?: null | DirectoryGwProbe; + location: Location; + mix_port: number; + name: string; + performance: string; + role: NodeRole; +}; + +export type DailyStats = { + date_utc: string; + total_packets_dropped: number; + total_packets_received: number; + total_packets_sent: number; + total_stake: number; +}; + +export type DeclaredRoles = { + entry: boolean; + exit_ipr: boolean; + exit_nr: boolean; + mixnode: boolean; +}; + +export type DescribedNodeType = + | "legacy_mixnode" + | "legacy_gateway" + | "nym_node"; + +export type DirectoryGwProbe = { + last_updated_utc: string; + outcome: ProbeOutcome; +}; + +export type Entry = EntryTestResult | null; + +export type EntryTestResult = { + can_connect: boolean; + can_route: boolean; +}; + +export type Exit = { + can_connect: boolean; + can_route_ip_external_v4: boolean; + can_route_ip_external_v6: boolean; + can_route_ip_v4: boolean; + can_route_ip_v6: boolean; +}; + +export type ExtendedNymNode = { + accepted_tnc: boolean; + bonded: boolean; + bonding_address?: string | null; + description: NodeDescription; + geoip?: null | NodeGeoData; + identity_key: string; + ip_address: string; + node_id: U32; + node_type: DescribedNodeType; + original_pledge: number; + rewarding_details?: null | NodeRewarding; + self_description: NymNodeData; + total_stake: string; + uptime: number; +}; + +export type Gateway = { + bonded: boolean; + config_score: number; + description: NodeDescription; + explorer_pretty_bond?: unknown; + gateway_identity_key: string; + last_probe_log?: string | null; + last_probe_result?: unknown; + last_testrun_utc?: string | null; + last_updated_utc: string; + performance: number; + routing_score: number; + self_described?: unknown; +}; + +export type GatewaySkinny = { + config_score: number; + explorer_pretty_bond?: unknown; + gateway_identity_key: string; + last_probe_result?: unknown; + last_testrun_utc?: string | null; + last_updated_utc: string; + performance: number; + routing_score: number; + self_described?: unknown; +}; + +export type GatewaySummary = { + bonded: GatewaySummaryBonded; + historical: GatewaySummaryHistorical; +}; + +export type GatewaySummaryBonded = { + count: number; + entry: number; + exit: number; + last_updated_utc: string; +}; + +export type GatewaySummaryHistorical = { + count: number; + last_updated_utc: string; +}; + +export type HealthInfo = { + uptime: number; +}; + +export type HostInformation = { + hostname?: string | null; + ip_address: Array; + keys: HostKeys; +}; + +export type HostKeys = { + ed25519: string; + x25519: string; + x25519_noise?: string; +}; + +export type IpPacketRouterDetails = { + /** + * address of the embedded ip packet router + */ + address: string; +}; + +/** + * based on + * https://github.com/nymtech/nym-vpn-client/blob/nym-vpn-core-v1.10.0/nym-vpn-core/crates/nym-gateway-probe/src/types.rs + * TODO: long term types should be moved into this repo because nym-vpn-client + * could pull it as a dependency and we'd have a single source of truth + */ +export type LastProbeResult = { + node: string; + outcome: ProbeOutcome; + used_entry: string; +}; + +export type Location = { + latitude: number; + longitude: number; + two_letter_iso_country_code: string; +}; + +export type MixingNodesSummary = { + count: number; + last_updated_utc: string; + legacy: number; + self_described: number; +}; + +export type Mixnode = { + bonded: boolean; + description: NodeDescription; + full_details?: unknown; + is_dp_delegatee: boolean; + last_updated_utc: string; + mix_id: number; + self_described?: unknown; + total_stake: number; +}; + +export type MixnodeSummary = { + bonded: MixingNodesSummary; + historical: MixnodeSummaryHistorical; +}; + +export type MixnodeSummaryHistorical = { + count: number; + last_updated_utc: string; +}; + +export type NetworkRequesterDetails = { + /** + * address of the embedded network requester + */ + address: string; + /** + * flag indicating whether this network requester uses the exit policy rather than the deprecated allow list + */ + uses_exit_policy: boolean; +}; + +export type NetworkSummary = { + gateways: GatewaySummary; + mixnodes: MixnodeSummary; + total_nodes: number; +}; + +/** + * The cost parameters, or the cost function, defined for the particular mixnode that influences + * how the rewards should be split between the node operator and its delegators. + */ +export type NodeCostParams = { + /** + * Operating cost of the associated node per the entire interval. + */ + interval_operating_cost: CoinSchema; + /** + * The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator. + */ + profit_margin_percent: string; +}; + +export type NodeDelegation = { + amount: CoinSchema; + block_height: number; + cumulative_reward_ratio: string; + owner: string; + proxy?: string | null; +}; + +export type NodeDescription = { + /** + * details define other optional details. + */ + details: string; + /** + * moniker defines a human-readable name for the node. + */ + moniker: string; + /** + * security contact defines an optional email for security contact. + */ + security_contact: string; + /** + * website defines an optional website link. + */ + website: string; +}; + +export type NodeGeoData = { + city: string; + country: string; + ip_address: string; + latitude: string; + longitude: string; + org: string; + postal: string; + region: string; + timezone: string; +}; + +export type NodeRewarding = { + /** + * Information provided by the operator that influence the cost function. + */ + cost_params: NodeCostParams; + /** + * Total delegation and compounded reward earned by all node delegators. + */ + delegates: string; + /** + * Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt + * to reward it multiple times in the same epoch. + */ + last_rewarded_epoch: U32; + /** + * Total pledge and compounded reward earned by the node operator. + */ + operator: string; + /** + * Cumulative reward earned by the "unit delegation" since the block 0. + */ + total_unit_reward: string; + unique_delegations: number; + /** + * Value of the theoretical "unit delegation" that has delegated to this node at block 0. + */ + unit_delegation: string; +}; + +export type NodeRole = + | { + Mixnode: { + layer: number; + }; + } + | "EntryGateway" + | "ExitGateway" + | "Standby" + | "Inactive"; + +export type NymNodeData = { + authenticator?: null | AuthenticatorDetails; + auxiliary_details?: AuxiliaryDetails; + build_information: BinaryBuildInformationOwned; + declared_role?: DeclaredRoles; + host_information: HostInformation; + ip_packet_router?: null | IpPacketRouterDetails; + last_polled?: OffsetDateTimeJsonSchemaWrapper; + mixnet_websockets: WebSockets; + network_requester?: null | NetworkRequesterDetails; + wireguard?: null | WireguardDetails; +}; + +export type OffsetDateTimeJsonSchemaWrapper = string; + +export type PagedResultExtendedNymNode = { + items: Array<{ + accepted_tnc: boolean; + bonded: boolean; + bonding_address?: string | null; + description: NodeDescription; + geoip?: null | NodeGeoData; + identity_key: string; + ip_address: string; + node_id: U32; + node_type: DescribedNodeType; + original_pledge: number; + rewarding_details?: null | NodeRewarding; + self_description: NymNodeData; + total_stake: string; + uptime: number; + }>; + page: number; + size: number; + total: number; +}; + +export type PagedResultGateway = { + items: Array<{ + bonded: boolean; + config_score: number; + description: NodeDescription; + explorer_pretty_bond?: unknown; + gateway_identity_key: string; + last_probe_log?: string | null; + last_probe_result?: unknown; + last_testrun_utc?: string | null; + last_updated_utc: string; + performance: number; + routing_score: number; + self_described?: unknown; + }>; + page: number; + size: number; + total: number; +}; + +export type PagedResultGatewaySkinny = { + items: Array<{ + config_score: number; + explorer_pretty_bond?: unknown; + gateway_identity_key: string; + last_probe_result?: unknown; + last_testrun_utc?: string | null; + last_updated_utc: string; + performance: number; + routing_score: number; + self_described?: unknown; + }>; + page: number; + size: number; + total: number; +}; + +export type PagedResultMixnode = { + items: Array<{ + bonded: boolean; + description: NodeDescription; + full_details?: unknown; + is_dp_delegatee: boolean; + last_updated_utc: string; + mix_id: number; + self_described?: unknown; + total_stake: number; + }>; + page: number; + size: number; + total: number; +}; + +export type PagedResultService = { + items: Array<{ + gateway_identity_key: string; + hostname?: string | null; + ip_address?: string | null; + last_successful_ping_utc?: string | null; + last_updated_utc: string; + mixnet_websockets?: unknown; + routing_score: number; + service_provider_client_id?: string | null; + }>; + page: number; + size: number; + total: number; +}; + +export type PagedResultSessionStats = { + items: Array<{ + day: string; + gateway_identity_key: string; + mixnet_sessions?: unknown; + node_id: number; + session_started: number; + unique_active_clients: number; + unknown_sessions?: unknown; + users_hashes?: unknown; + vpn_sessions?: unknown; + }>; + page: number; + size: number; + total: number; +}; + +export type ProbeOutcome = { + as_entry: Entry; + as_exit?: null | Exit; + wg?: null | ProbeOutcomeV1; +}; + +export type ProbeOutcomeV1 = { + can_handshake_v4: boolean; + can_handshake_v6: boolean; + can_register: boolean; + can_resolve_dns_v4: boolean; + can_resolve_dns_v6: boolean; + download_duration_sec_v4: number; + download_duration_sec_v6: number; + download_error_v4: string; + download_error_v6: string; + downloaded_file_v4: string; + downloaded_file_v6: string; + ping_hosts_performance_v4: number; + ping_hosts_performance_v6: number; + ping_ips_performance_v4: number; + ping_ips_performance_v6: number; +}; + +export type Service = { + gateway_identity_key: string; + hostname?: string | null; + ip_address?: string | null; + last_successful_ping_utc?: string | null; + last_updated_utc: string; + mixnet_websockets?: unknown; + routing_score: number; + service_provider_client_id?: string | null; +}; + +export type SessionStats = { + day: string; + gateway_identity_key: string; + mixnet_sessions?: unknown; + node_id: number; + session_started: number; + unique_active_clients: number; + unknown_sessions?: unknown; + users_hashes?: unknown; + vpn_sessions?: unknown; +}; + +export type SummaryHistory = { + date: string; + timestamp_utc: string; + value_json: unknown; +}; + +export type TestRun = { + id: number; + identity_key: string; + log: string; + status: string; +}; + +export type WebSockets = { + ws_port: number; + wss_port?: number | null; +}; + +export type WireguardDetails = { + port: number; + public_key: string; +}; + +export type U32 = number; + +export type GetGatewaysData = { + body?: never; + path?: never; + query?: { + min_node_version?: string; + }; + url: "/dvpn/v1/directory/gateways"; +}; + +export type GetGatewaysResponses = { + 200: Array; +}; + +export type GetGatewaysResponse = + GetGatewaysResponses[keyof GetGatewaysResponses]; + +export type GetGatewayCountriesData = { + body?: never; + path?: never; + query?: never; + url: "/dvpn/v1/directory/gateways/countries"; +}; + +export type GetGatewayCountriesResponses = { + 200: Array; +}; + +export type GetGatewayCountriesResponse = + GetGatewayCountriesResponses[keyof GetGatewayCountriesResponses]; + +export type GetGatewaysByCountryData = { + body?: never; + path: { + two_letter_country_code: string; + }; + query?: never; + url: "/dvpn/v1/directory/gateways/country/{two_letter_country_code}"; +}; + +export type GetGatewaysByCountryResponses = { + 200: Array; +}; + +export type GetGatewaysByCountryResponse = + GetGatewaysByCountryResponses[keyof GetGatewaysByCountryResponses]; + +export type GetEntryGatewaysData = { + body?: never; + path?: never; + query?: never; + url: "/dvpn/v1/directory/gateways/entry"; +}; + +export type GetEntryGatewaysResponses = { + 200: Array; +}; + +export type GetEntryGatewaysResponse = + GetEntryGatewaysResponses[keyof GetEntryGatewaysResponses]; + +export type GetEntryGatewayCountriesData = { + body?: never; + path?: never; + query?: never; + url: "/dvpn/v1/directory/gateways/entry/countries"; +}; + +export type GetEntryGatewayCountriesResponses = { + 200: Array; +}; + +export type GetEntryGatewayCountriesResponse = + GetEntryGatewayCountriesResponses[keyof GetEntryGatewayCountriesResponses]; + +export type GetEntryGatewaysByCountryData = { + body?: never; + path: { + two_letter_country_code: string; + }; + query?: never; + url: "/dvpn/v1/directory/gateways/entry/country/{two_letter_country_code}"; +}; + +export type GetEntryGatewaysByCountryResponses = { + 200: Array; +}; + +export type GetEntryGatewaysByCountryResponse = + GetEntryGatewaysByCountryResponses[keyof GetEntryGatewaysByCountryResponses]; + +export type GetExitGatewaysData = { + body?: never; + path?: never; + query?: never; + url: "/dvpn/v1/directory/gateways/exit"; +}; + +export type GetExitGatewaysResponses = { + 200: Array; +}; + +export type GetExitGatewaysResponse = + GetExitGatewaysResponses[keyof GetExitGatewaysResponses]; + +export type GetExitGatewayCountriesData = { + body?: never; + path?: never; + query?: never; + url: "/dvpn/v1/directory/gateways/exit/countries"; +}; + +export type GetExitGatewayCountriesResponses = { + 200: Array; +}; + +export type GetExitGatewayCountriesResponse = + GetExitGatewayCountriesResponses[keyof GetExitGatewayCountriesResponses]; + +export type GetExitGatewaysByCountryData = { + body?: never; + path: { + two_letter_country_code: string; + }; + query?: never; + url: "/dvpn/v1/directory/gateways/exit/country/{two_letter_country_code}"; +}; + +export type GetExitGatewaysByCountryResponses = { + 200: Array; +}; + +export type GetExitGatewaysByCountryResponse = + GetExitGatewaysByCountryResponses[keyof GetExitGatewaysByCountryResponses]; + +export type NymNodesData = { + body?: never; + path?: never; + query?: { + size?: number; + page?: number; + }; + url: "/explorer/v3/nym-nodes"; +}; + +export type NymNodesResponses = { + 200: PagedResultExtendedNymNode; +}; + +export type NymNodesResponse = NymNodesResponses[keyof NymNodesResponses]; + +export type NodeDelegationsData = { + body?: never; + path: { + node_id: U32; + }; + query?: never; + url: "/explorer/v3/nym-nodes/{node_id}/delegations"; +}; + +export type NodeDelegationsResponses = { + 200: NodeDelegation; +}; + +export type NodeDelegationsResponse = + NodeDelegationsResponses[keyof NodeDelegationsResponses]; + +export type GatewaysData = { + body?: never; + path?: never; + query?: { + size?: number; + page?: number; + }; + url: "/v2/gateways"; +}; + +export type GatewaysResponses = { + 200: PagedResultGateway; +}; + +export type GatewaysResponse = GatewaysResponses[keyof GatewaysResponses]; + +export type GatewaysSkinnyData = { + body?: never; + path?: never; + query?: { + size?: number; + page?: number; + }; + url: "/v2/gateways/skinny"; +}; + +export type GatewaysSkinnyResponses = { + 200: PagedResultGatewaySkinny; +}; + +export type GatewaysSkinnyResponse = + GatewaysSkinnyResponses[keyof GatewaysSkinnyResponses]; + +export type GetGatewayData = { + body?: never; + path: { + identity_key: string; + }; + query?: never; + url: "/v2/gateways/{identity_key}"; +}; + +export type GetGatewayResponses = { + 200: Gateway; +}; + +export type GetGatewayResponse = GetGatewayResponses[keyof GetGatewayResponses]; + +export type GetAllSessionsData = { + body?: never; + path?: never; + query?: { + size?: number; + page?: number; + node_id?: string; + day?: string; + }; + url: "/v2/metrics/sessions"; +}; + +export type GetAllSessionsResponses = { + 200: PagedResultSessionStats; +}; + +export type GetAllSessionsResponse = + GetAllSessionsResponses[keyof GetAllSessionsResponses]; + +export type MixnodesData = { + body?: never; + path?: never; + query?: { + size?: number; + page?: number; + }; + url: "/v2/mixnodes"; +}; + +export type MixnodesResponses = { + 200: PagedResultMixnode; +}; + +export type MixnodesResponse = MixnodesResponses[keyof MixnodesResponses]; + +export type GetStatsData = { + body?: never; + path?: never; + query?: { + offset?: number; + }; + url: "/v2/mixnodes/stats"; +}; + +export type GetStatsResponses = { + 200: Array; +}; + +export type GetStatsResponse = GetStatsResponses[keyof GetStatsResponses]; + +export type GetMixnodesData = { + body?: never; + path: { + mix_id: string; + }; + query?: never; + url: "/v2/mixnodes/{mix_id}"; +}; + +export type GetMixnodesResponses = { + 200: Mixnode; +}; + +export type GetMixnodesResponse = + GetMixnodesResponses[keyof GetMixnodesResponses]; + +export type Mixnodes2Data = { + body?: never; + path?: never; + query?: { + size?: number; + page?: number; + wss?: boolean; + hostname?: boolean; + entry?: boolean; + }; + url: "/v2/services"; +}; + +export type Mixnodes2Responses = { + 200: PagedResultService; +}; + +export type Mixnodes2Response = Mixnodes2Responses[keyof Mixnodes2Responses]; + +export type BuildInformationData = { + body?: never; + path?: never; + query?: never; + url: "/v2/status/build_information"; +}; + +export type BuildInformationResponses = { + 200: BinaryBuildInformationOwned; +}; + +export type BuildInformationResponse = + BuildInformationResponses[keyof BuildInformationResponses]; + +export type HealthData = { + body?: never; + path?: never; + query?: never; + url: "/v2/status/health"; +}; + +export type HealthResponses = { + 200: HealthInfo; +}; + +export type HealthResponse = HealthResponses[keyof HealthResponses]; + +export type SummaryData = { + body?: never; + path?: never; + query?: never; + url: "/v2/summary"; +}; + +export type SummaryResponses = { + 200: NetworkSummary; +}; + +export type SummaryResponse = SummaryResponses[keyof SummaryResponses]; + +export type SummaryHistoryData = { + body?: never; + path?: never; + query?: never; + url: "/v2/summary/history"; +}; + +export type SummaryHistoryResponses = { + 200: Array; +}; + +export type SummaryHistoryResponse = + SummaryHistoryResponses[keyof SummaryHistoryResponses]; + +export type ClientOptions = { + baseUrl: "https://mainnet-node-status-api.nymtech.cc" | (string & {}); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/components/CardAlert.tsx b/nym-node-status-api/nym-node-status-ui/src/components/CardAlert.tsx new file mode 100644 index 0000000000..9a7a13ac0b --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/CardAlert.tsx @@ -0,0 +1,25 @@ +"use client"; +import AutoAwesomeRoundedIcon from "@mui/icons-material/AutoAwesomeRounded"; +import Button from "@mui/material/Button"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; + +export default function CardAlert() { + return ( + + + + + Plan about to expire + + + Enjoy 10% off when renewing your plan today. + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/Copyright.tsx b/nym-node-status-api/nym-node-status-ui/src/components/Copyright.tsx new file mode 100644 index 0000000000..e0167a578a --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/Copyright.tsx @@ -0,0 +1,24 @@ +import Link from "@mui/material/Link"; +import Typography from "@mui/material/Typography"; + +export default function Copyright(props: any) { + return ( + + {"Copyright © "} + + Nym Technologies SA + {" "} + {new Date().getFullYear()} + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/GraphCard.tsx b/nym-node-status-api/nym-node-status-ui/src/components/GraphCard.tsx new file mode 100644 index 0000000000..ee5ea54091 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/GraphCard.tsx @@ -0,0 +1,23 @@ +"use client"; + +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; + +export type GraphProps = { + title: string; + children?: React.ReactNode; +}; + +export default function GraphCard({ title, children }: GraphProps) { + return ( + + + + {title} + + {children} + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/HighlightedCard.tsx b/nym-node-status-api/nym-node-status-ui/src/components/HighlightedCard.tsx new file mode 100644 index 0000000000..f551b85e3d --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/HighlightedCard.tsx @@ -0,0 +1,42 @@ +"use client"; +import ChevronRightRoundedIcon from "@mui/icons-material/ChevronRightRounded"; +import InsightsRoundedIcon from "@mui/icons-material/InsightsRounded"; +import Button from "@mui/material/Button"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; + +export default function HighlightedCard() { + const theme = useTheme(); + const isSmallScreen = useMediaQuery(theme.breakpoints.down("sm")); + + return ( + + + + + Explore your data + + + Uncover performance and visitor insights with our data wizardry. + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/ScoreIcon.tsx b/nym-node-status-api/nym-node-status-ui/src/components/ScoreIcon.tsx new file mode 100644 index 0000000000..d1c47c549f --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/ScoreIcon.tsx @@ -0,0 +1,36 @@ +import SignalCellularAltIcon from "@mui/icons-material/SignalCellularAlt"; +import SignalCellularAlt1Bar from "@mui/icons-material/SignalCellularAlt1Bar"; +import SignalCellularAlt2Bar from "@mui/icons-material/SignalCellularAlt2Bar"; +import SignalCellularConnectedNoInternet0BarIcon from "@mui/icons-material/SignalCellularConnectedNoInternet0Bar"; + +export const ScoreIcon = ({ score }: { score?: string }) => { + if (!score) { + return ; + } + if (score.toLowerCase() === "offline") { + return ; + } + if (score.toLowerCase() === "high") { + return ; + } + if (score.toLowerCase() === "medium") { + return ; + } + return ; +}; + +export const ReverseScoreIcon = ({ score }: { score?: string }) => { + if (!score) { + return ; + } + if (score.toLowerCase() === "offline") { + return ; + } + if (score.toLowerCase() === "low") { + return ; + } + if (score.toLowerCase() === "medium") { + return ; + } + return ; +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/components/StatCard.tsx b/nym-node-status-api/nym-node-status-ui/src/components/StatCard.tsx new file mode 100644 index 0000000000..0ecf2514a0 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/StatCard.tsx @@ -0,0 +1,130 @@ +"use client"; + +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Chip from "@mui/material/Chip"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import { areaElementClasses } from "@mui/x-charts/LineChart"; +import { SparkLineChart } from "@mui/x-charts/SparkLineChart"; + +export type StatCardProps = { + title: string; + value: string; + interval: string; + trend: "up" | "down" | "neutral"; + data: number[]; +}; + +function getDaysInMonth(month: number, year: number) { + const date = new Date(year, month, 0); + const monthName = date.toLocaleDateString("en-US", { + month: "short", + }); + const daysInMonth = date.getDate(); + const days = []; + let i = 1; + while (days.length < daysInMonth) { + days.push(`${monthName} ${i}`); + i += 1; + } + return days; +} + +function AreaGradient({ color, id }: { color: string; id: string }) { + return ( + + + + + + + ); +} + +export default function StatCard({ + title, + value, + interval, + trend, + data, +}: StatCardProps) { + const theme = useTheme(); + const daysInWeek = getDaysInMonth(4, 2024); + + const trendColors = { + up: + theme.palette.mode === "light" + ? theme.palette.success.main + : theme.palette.success.dark, + down: + theme.palette.mode === "light" + ? theme.palette.error.main + : theme.palette.error.dark, + neutral: + theme.palette.mode === "light" + ? theme.palette.grey[400] + : theme.palette.grey[700], + }; + + const labelColors = { + up: "success" as const, + down: "error" as const, + neutral: "default" as const, + }; + + const color = labelColors[trend]; + const chartColor = trendColors[trend]; + const trendValues = { up: "+25%", down: "-25%", neutral: "+5%" }; + + return ( + + + + {title} + + + + + + {value} + + + + + {interval} + + + + + + + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayCanQueryMetadataTopup.tsx b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayCanQueryMetadataTopup.tsx new file mode 100644 index 0000000000..30f066e937 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayCanQueryMetadataTopup.tsx @@ -0,0 +1,58 @@ +import { useDVpnGatewaysTransformed } from "@/hooks/useGatewaysTransformed"; +import Box from "@mui/material/Box"; +import { BarChart } from "@mui/x-charts/BarChart"; +import { rollup } from "d3-array"; +import React from "react"; + +export const GatewayCanQueryMetadataTopup = () => { + const { + query: { isSuccess, isError, data }, + } = useDVpnGatewaysTransformed(); + const binnedData = React.useMemo(() => { + if (!isSuccess || data === undefined) { + return undefined; + } + const results = data.map((g) => { + const r = (g.last_probe?.outcome.wg as any)?.can_query_metadata_v4; + if (r === undefined) { + return "-"; + } + if (r === true) { + return "yes"; + } + return "no"; + }); + // count occurrences of each result + const resultCounts = rollup( + results, + (v) => v.length, + (v) => v, // group by result string + ); + + const chartData = Array.from(resultCounts, ([result, count]) => ({ + result, + count, + })); + + const labels = chartData.map((d) => d.result); + const values = chartData.map((d) => d.count); + + return { labels, values }; + }, [data, isSuccess]); + + if (isError || !binnedData) { + return null; + } + + const { labels, values } = binnedData; + + return ( + + + + ); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayDownloadSpeeds.tsx b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayDownloadSpeeds.tsx new file mode 100644 index 0000000000..17ad7c128f --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayDownloadSpeeds.tsx @@ -0,0 +1,43 @@ +import { useDVpnGatewaysTransformed } from "@/hooks/useGatewaysTransformed"; +import Box from "@mui/material/Box"; +import { BarChart } from "@mui/x-charts/BarChart"; +import { bin } from "d3-array"; +import React from "react"; + +export const GatewayDownloadSpeeds = () => { + const { + query: { isSuccess, isError, data }, + } = useDVpnGatewaysTransformed(); + const binnedData = React.useMemo(() => { + if (!isSuccess || data === undefined) { + return undefined; + } + const binner = bin().thresholds(10); // Number of bins + const bins = binner( + data + .map((g) => g.extra.downloadSpeedMBPerSec) + .filter((g) => Boolean(g)) as number[], + ); + + const labels = bins.map((b) => `${b.x0}-${b.x1} MB/sec`); + const values = bins.map((b) => b.length); // count per bin + + return { labels, values }; + }, [data, isSuccess]); + + if (isError || !binnedData) { + return null; + } + + const { labels, values } = binnedData; + + return ( + + + + ); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayLoads.tsx b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayLoads.tsx new file mode 100644 index 0000000000..f3c201dce9 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayLoads.tsx @@ -0,0 +1,45 @@ +import { useDVpnGatewaysTransformed } from "@/hooks/useGatewaysTransformed"; +import Box from "@mui/material/Box"; +import { BarChart } from "@mui/x-charts/BarChart"; +import React from "react"; + +export const GatewayLoads = () => { + const { + query: { isSuccess, isError, data }, + } = useDVpnGatewaysTransformed(); + const binnedData = React.useMemo(() => { + if (!isSuccess || data === undefined) { + return undefined; + } + const binned = data.reduce( + (acc, g) => { + const score: "low" | "medium" | "high" | "offline" = + (g as any).performance_v2?.load || "offline"; + acc[score] += 1; + return acc; + }, + { offline: 0, low: 0, medium: 0, high: 0 }, + ); + + const labels = ["offline", "low", "medium", "high"]; + const values = Object.values(binned); + + return { labels, values }; + }, [data, isSuccess]); + + if (isError || !binnedData) { + return null; + } + + const { labels, values } = binnedData; + + return ( + + + + ); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayPingPercentage.tsx b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayPingPercentage.tsx new file mode 100644 index 0000000000..38bca5f5c5 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayPingPercentage.tsx @@ -0,0 +1,43 @@ +import { useDVpnGatewaysTransformed } from "@/hooks/useGatewaysTransformed"; +import Box from "@mui/material/Box"; +import { BarChart } from "@mui/x-charts/BarChart"; +import { bin } from "d3-array"; +import React from "react"; + +export const GatewayPingPercentage = () => { + const { + query: { isSuccess, isError, data }, + } = useDVpnGatewaysTransformed(); + const binnedData = React.useMemo(() => { + if (!isSuccess || data === undefined) { + return undefined; + } + const binner = bin().domain([0, 1]).thresholds(10); // Number of bins + const bins = binner( + data.map((g) => g.last_probe?.outcome.wg?.ping_ips_performance_v4 || 0), + ); + + const labels = bins.map( + (b) => `${(b.x0 || 0) * 100}-${(b.x1 || 0) * 100}%`, + ); + const values = bins.map((b) => b.length); // count per bin + + return { labels, values }; + }, [data, isSuccess]); + + if (isError || !binnedData) { + return null; + } + + const { labels, values } = binnedData; + + return ( + + + + ); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayScores.tsx b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayScores.tsx new file mode 100644 index 0000000000..3daa8f4a67 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayScores.tsx @@ -0,0 +1,45 @@ +import { useDVpnGatewaysTransformed } from "@/hooks/useGatewaysTransformed"; +import Box from "@mui/material/Box"; +import { BarChart } from "@mui/x-charts/BarChart"; +import React from "react"; + +export const GatewayScores = () => { + const { + query: { isSuccess, isError, data }, + } = useDVpnGatewaysTransformed(); + const binnedData = React.useMemo(() => { + if (!isSuccess || data === undefined) { + return undefined; + } + const binned = data.reduce( + (acc, g) => { + const score: "low" | "medium" | "high" | "offline" = + (g as any).performance_v2?.score || "offline"; + acc[score] += 1; + return acc; + }, + { offline: 0, low: 0, medium: 0, high: 0 }, + ); + + const labels = ["offline", "low", "medium", "high"]; + const values = Object.values(binned); + + return { labels, values }; + }, [data, isSuccess]); + + if (isError || !binnedData) { + return null; + } + + const { labels, values } = binnedData; + + return ( + + + + ); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayUptimePercentage.tsx b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayUptimePercentage.tsx new file mode 100644 index 0000000000..d84f707316 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayUptimePercentage.tsx @@ -0,0 +1,41 @@ +import { useDVpnGatewaysTransformed } from "@/hooks/useGatewaysTransformed"; +import Box from "@mui/material/Box"; +import { BarChart } from "@mui/x-charts/BarChart"; +import { bin } from "d3-array"; +import React from "react"; + +export const GatewayUptimePercentage = () => { + const { + query: { isSuccess, isError, data }, + } = useDVpnGatewaysTransformed(); + const binnedData = React.useMemo(() => { + if (!isSuccess || data === undefined) { + return undefined; + } + const binner = bin().domain([0, 1]).thresholds(20); // Number of bins + const bins = binner(data.map((g) => Number.parseFloat(g.performance))); + + const labels = bins.map( + (b) => `${(b.x0 || 0) * 100}-${(b.x1 || 0) * 100}%`, + ); + const values = bins.map((b) => b.length); // count per bin + + return { labels, values }; + }, [data, isSuccess]); + + if (isError || !binnedData) { + return null; + } + + const { labels, values } = binnedData; + + return ( + + + + ); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayVersions.tsx b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayVersions.tsx new file mode 100644 index 0000000000..a6e9d0f3c7 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayVersions.tsx @@ -0,0 +1,49 @@ +import { useDVpnGatewaysTransformed } from "@/hooks/useGatewaysTransformed"; +import Box from "@mui/material/Box"; +import { BarChart } from "@mui/x-charts/BarChart"; +import { rollup } from "d3-array"; +import React from "react"; + +export const GatewayVersions = () => { + const { + query: { isSuccess, isError, data }, + } = useDVpnGatewaysTransformed(); + const binnedData = React.useMemo(() => { + if (!isSuccess || data === undefined) { + return undefined; + } + const versions = data.map((g) => g.build_information.build_version); + // count occurrences of each version + const versionCounts = rollup( + versions, + (v) => v.length, + (v) => v, // group by version string + ); + + const chartData = Array.from(versionCounts, ([version, count]) => ({ + version, + count, + })); + + const labels = chartData.map((d) => d.version); + const values = chartData.map((d) => d.count); + + return { labels, values }; + }, [data, isSuccess]); + + if (isError || !binnedData) { + return null; + } + + const { labels, values } = binnedData; + + return ( + + + + ); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/AppNavbar.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/AppNavbar.tsx new file mode 100644 index 0000000000..5f978551f4 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/AppNavbar.tsx @@ -0,0 +1,77 @@ +"use client"; + +import ColorModeIconDropdown from "@/theme/ColorModeIconDropdown"; +import MenuRoundedIcon from "@mui/icons-material/MenuRounded"; +import AppBar from "@mui/material/AppBar"; +import Stack from "@mui/material/Stack"; +import { tabsClasses } from "@mui/material/Tabs"; +import MuiToolbar from "@mui/material/Toolbar"; +import { styled } from "@mui/material/styles"; +import * as React from "react"; +import MenuButton from "./MenuButton"; +import SideMenuMobile from "./SideMenuMobile"; +import { SiteLogo } from "./SiteLogo"; + +const Toolbar = styled(MuiToolbar)({ + width: "100%", + padding: "12px", + display: "flex", + flexDirection: "column", + alignItems: "start", + justifyContent: "center", + gap: "12px", + flexShrink: 0, + [`& ${tabsClasses.flexContainer}`]: { + gap: "8px", + p: "8px", + pb: 0, + }, +}); + +export default function AppNavbar() { + const [open, setOpen] = React.useState(false); + + const toggleDrawer = (newOpen: boolean) => () => { + setOpen(newOpen); + }; + + return ( + + + + + + + + + + + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/ColorModeIconDropdown.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/ColorModeIconDropdown.tsx new file mode 100644 index 0000000000..dd1ebef7d4 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/ColorModeIconDropdown.tsx @@ -0,0 +1,91 @@ +"use client"; + +import DarkModeIcon from "@mui/icons-material/DarkModeRounded"; +import LightModeIcon from "@mui/icons-material/LightModeRounded"; +import Box from "@mui/material/Box"; +import IconButton, { type IconButtonOwnProps } from "@mui/material/IconButton"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import { useColorScheme } from "@mui/material/styles"; +import * as React from "react"; + +export default function ColorModeIconDropdown(props: IconButtonOwnProps) { + const { mode, systemMode, setMode } = useColorScheme(); + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + const handleMode = (targetMode: "system" | "light" | "dark") => () => { + setMode(targetMode); + handleClose(); + }; + if (!mode) { + return ( + ({ + verticalAlign: "bottom", + display: "inline-flex", + width: "2.25rem", + height: "2.25rem", + borderRadius: theme.shape.borderRadius, + border: "1px solid", + borderColor: theme.palette.divider, + })} + /> + ); + } + const resolvedMode = (systemMode || mode) as "light" | "dark"; + const icon = { + light: , + dark: , + }[resolvedMode]; + return ( + + + {icon} + + + + System + + + Light + + + Dark + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/ColorModeSelect.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/ColorModeSelect.tsx new file mode 100644 index 0000000000..839b3b6dc3 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/ColorModeSelect.tsx @@ -0,0 +1,29 @@ +"use client"; + +import MenuItem from "@mui/material/MenuItem"; +import Select, { type SelectProps } from "@mui/material/Select"; +import { useColorScheme } from "@mui/material/styles"; + +export default function ColorModeSelect(props: SelectProps) { + const { mode, setMode } = useColorScheme(); + if (!mode) { + return null; + } + return ( + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/Header.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/Header.tsx new file mode 100644 index 0000000000..5d7ebe12c2 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/Header.tsx @@ -0,0 +1,33 @@ +"use client"; + +import ColorModeIconDropdown from "@/theme/ColorModeIconDropdown"; +import Stack from "@mui/material/Stack"; +import NavbarBreadcrumbs from "./NavbarBreadcrumbs"; + +import type React from "react"; +import Search from "./Search"; + +const showSearch = false; + +export default function Header({ title }: { title?: React.ReactNode }) { + return ( + + + + {showSearch && } + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/MenuButton.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/MenuButton.tsx new file mode 100644 index 0000000000..c628996e25 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/MenuButton.tsx @@ -0,0 +1,23 @@ +"use client"; +import Badge, { badgeClasses } from "@mui/material/Badge"; +import IconButton, { type IconButtonProps } from "@mui/material/IconButton"; + +export interface MenuButtonProps extends IconButtonProps { + showBadge?: boolean; +} + +export default function MenuButton({ + showBadge = false, + ...props +}: MenuButtonProps) { + return ( + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/MenuContent.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/MenuContent.tsx new file mode 100644 index 0000000000..a38b662c88 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/MenuContent.tsx @@ -0,0 +1,60 @@ +"use client"; + +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import Stack from "@mui/material/Stack"; +import NextLink from "next/link"; +import { usePathname } from "next/navigation"; + +import DoorSlidingOutlinedIcon from "@mui/icons-material/DoorSlidingOutlined"; +import HubIcon from "@mui/icons-material/Hub"; +import SettingsInputAntennaIcon from "@mui/icons-material/SettingsInputAntenna"; +import ViewModuleIcon from "@mui/icons-material/ViewModule"; +import WorkspacePremiumIcon from "@mui/icons-material/WorkspacePremium"; + +const mainListItemsAll = [ + { text: "Network Nodes", icon: , url: "/nodes" }, + { text: "dVPN Gateways", icon: , url: "/dvpn" }, + { text: "SOCKS5 NRs", icon: , url: "/socks5" }, + { + text: "zk-nym Signers", + icon: , + url: "/zk-nym-signers", + }, + { + text: "Nyx Chain Validators", + icon: , + url: "/validators", + }, +]; + +const mainListItems = [mainListItemsAll[0], mainListItemsAll[1]]; + +export default function MenuContent() { + const path = usePathname(); + return ( + + + {mainListItems.map((item, index) => ( + + + {item.icon} + + + + ))} + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/NavbarBreadcrumbs.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/NavbarBreadcrumbs.tsx new file mode 100644 index 0000000000..b74145778e --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/NavbarBreadcrumbs.tsx @@ -0,0 +1,37 @@ +"use client"; + +import NavigateNextRoundedIcon from "@mui/icons-material/NavigateNextRounded"; +import Breadcrumbs, { breadcrumbsClasses } from "@mui/material/Breadcrumbs"; +import Typography from "@mui/material/Typography"; +import { styled } from "@mui/material/styles"; +import type React from "react"; + +const StyledBreadcrumbs = styled(Breadcrumbs)(({ theme }) => ({ + margin: theme.spacing(1, 0), + [`& .${breadcrumbsClasses.separator}`]: { + color: theme.palette.action.disabled, + margin: 1, + }, + [`& .${breadcrumbsClasses.ol}`]: { + alignItems: "center", + }, +})); + +export default function NavbarBreadcrumbs({ + title, +}: { title?: React.ReactNode }) { + return ( + } + > + Dashboard + + {title || "Home"} + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/OptionsMenu.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/OptionsMenu.tsx new file mode 100644 index 0000000000..940ad44148 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/OptionsMenu.tsx @@ -0,0 +1,81 @@ +"use client"; + +import LogoutRoundedIcon from "@mui/icons-material/LogoutRounded"; +import MoreVertRoundedIcon from "@mui/icons-material/MoreVertRounded"; +import Divider, { dividerClasses } from "@mui/material/Divider"; +import { listClasses } from "@mui/material/List"; +import ListItemIcon, { listItemIconClasses } from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import Menu from "@mui/material/Menu"; +import MuiMenuItem from "@mui/material/MenuItem"; +import { paperClasses } from "@mui/material/Paper"; +import { styled } from "@mui/material/styles"; +import * as React from "react"; +import MenuButton from "./MenuButton"; + +const MenuItem = styled(MuiMenuItem)({ + margin: "2px 0", +}); + +export default function OptionsMenu() { + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + return ( + + + + + + Profile + My account + + Add another account + Settings + + + Logout + + + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/Search.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/Search.tsx new file mode 100644 index 0000000000..78d95c6bdb --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/Search.tsx @@ -0,0 +1,27 @@ +"use client"; + +import SearchRoundedIcon from "@mui/icons-material/SearchRounded"; +import FormControl from "@mui/material/FormControl"; +import InputAdornment from "@mui/material/InputAdornment"; +import OutlinedInput from "@mui/material/OutlinedInput"; + +export default function Search() { + return ( + + + + + } + inputProps={{ + "aria-label": "search", + }} + /> + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/SelectContent.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/SelectContent.tsx new file mode 100644 index 0000000000..3b0d2c2ed2 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/SelectContent.tsx @@ -0,0 +1,107 @@ +"use client"; + +import AddRoundedIcon from "@mui/icons-material/AddRounded"; +import ConstructionRoundedIcon from "@mui/icons-material/ConstructionRounded"; +import DevicesRoundedIcon from "@mui/icons-material/DevicesRounded"; +import SmartphoneRoundedIcon from "@mui/icons-material/SmartphoneRounded"; +import MuiAvatar from "@mui/material/Avatar"; +import Divider from "@mui/material/Divider"; +import MuiListItemAvatar from "@mui/material/ListItemAvatar"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import ListSubheader from "@mui/material/ListSubheader"; +import MenuItem from "@mui/material/MenuItem"; +import Select, { + type SelectChangeEvent, + selectClasses, +} from "@mui/material/Select"; +import { styled } from "@mui/material/styles"; +import * as React from "react"; + +const Avatar = styled(MuiAvatar)(({ theme }) => ({ + width: 28, + height: 28, + backgroundColor: theme.palette.background.paper, + color: theme.palette.text.secondary, + border: `1px solid ${theme.palette.divider}`, +})); + +const ListItemAvatar = styled(MuiListItemAvatar)({ + minWidth: 0, + marginRight: 12, +}); + +export default function SelectContent() { + const [company, setCompany] = React.useState(""); + + const handleChange = (event: SelectChangeEvent) => { + setCompany(event.target.value as string); + }; + + return ( + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/SideMenu.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/SideMenu.tsx new file mode 100644 index 0000000000..047e2b696d --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/SideMenu.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { SiteLogo } from "@/components/nav/SiteLogo"; +import Box from "@mui/material/Box"; +import Divider from "@mui/material/Divider"; +import MuiDrawer, { drawerClasses } from "@mui/material/Drawer"; +import { styled } from "@mui/material/styles"; +import MenuContent from "./MenuContent"; + +const drawerWidth = 240; + +const Drawer = styled(MuiDrawer)({ + width: drawerWidth, + flexShrink: 0, + boxSizing: "border-box", + mt: 10, + [`& .${drawerClasses.paper}`]: { + width: drawerWidth, + boxSizing: "border-box", + }, +}); + +export default function SideMenu() { + return ( + + + + + + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/SideMenuMobile.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/SideMenuMobile.tsx new file mode 100644 index 0000000000..f6e866af5a --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/SideMenuMobile.tsx @@ -0,0 +1,43 @@ +"use client"; + +import Divider from "@mui/material/Divider"; +import Drawer, { drawerClasses } from "@mui/material/Drawer"; +import Stack from "@mui/material/Stack"; +import MenuContent from "./MenuContent"; + +interface SideMenuMobileProps { + open: boolean | undefined; + toggleDrawer: (newOpen: boolean) => () => void; +} + +export default function SideMenuMobile({ + open, + toggleDrawer, +}: SideMenuMobileProps) { + return ( + theme.zIndex.drawer + 1, + [`& .${drawerClasses.paper}`]: { + backgroundImage: "none", + backgroundColor: "background.paper", + }, + }} + > + + + + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/SiteLogo.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/SiteLogo.tsx new file mode 100644 index 0000000000..336ee3795c --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/SiteLogo.tsx @@ -0,0 +1,15 @@ +import Link from "@mui/material/Link"; +import NextLink from "next/link"; + +export function SiteLogo() { + return ( + + Nym Node Status + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/context/queryContext.tsx b/nym-node-status-api/nym-node-status-ui/src/context/queryContext.tsx new file mode 100644 index 0000000000..43578ed17f --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/context/queryContext.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { type Client, createClient } from "@/client/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import type React from "react"; +import { createContext, useContext, useRef } from "react"; + +interface State { + client?: Client; +} + +export const QueryContext = createContext({ + client: undefined, +}); + +export const useQueryContext = (): React.ContextType => + useContext(QueryContext); + +export const QueryContextProvider = ({ + children, +}: { + children: React.ReactNode | React.ReactNode[]; +}) => { + const openApiClient = useRef( + createClient({ baseUrl: "https://mainnet-node-status-api.nymtech.cc" }), + ); + const queryClient = useRef(new QueryClient()); + + const state: State = { + client: openApiClient.current, + }; + + return ( + + + {children} + {/* Add devtools in development */} + {process.env.NODE_ENV === "development" && ( + + )} + + + ); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/hooks/types.ts b/nym-node-status-api/nym-node-status-ui/src/hooks/types.ts new file mode 100644 index 0000000000..852d109cc7 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/hooks/types.ts @@ -0,0 +1,4 @@ +export interface Pagination { + pageIndex?: number; + pageSize?: number; +} diff --git a/nym-node-status-api/nym-node-status-ui/src/hooks/useAllNymNodes.ts b/nym-node-status-api/nym-node-status-ui/src/hooks/useAllNymNodes.ts new file mode 100644 index 0000000000..05bfa0a5cf --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/hooks/useAllNymNodes.ts @@ -0,0 +1,39 @@ +import { nymNodes } from "@/client/sdk.gen"; +import { useQueryContext } from "@/context/queryContext"; +import type { NymNode } from "@/hooks/useNymNodes"; +import { useQuery } from "@tanstack/react-query"; +import React from "react"; + +export const useAllNymNodes = () => { + const { client } = useQueryContext(); + const key = "nym-nodes-all"; + + const queryFn = React.useCallback(async (): Promise => { + const size = 100; + let busy = true; + let page = 0; + const allData = []; + do { + const { data, error } = await nymNodes({ client, query: { page, size } }); + if (error) throw error; + + if (data?.items) { + allData.push(...data.items); + } + + // keep querying until data is less than a page + if ((data?.items.length || 0) < size) { + busy = false; + } + page += 1; + } while (busy); + return allData; + }, [client]); + + const query = useQuery({ queryKey: [key], queryFn }); + + return { + key, + query, + }; +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/hooks/useGateways.ts b/nym-node-status-api/nym-node-status-ui/src/hooks/useGateways.ts new file mode 100644 index 0000000000..d1508ce7da --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/hooks/useGateways.ts @@ -0,0 +1,20 @@ +import { getGatewaysOptions } from "@/client/@tanstack/react-query.gen"; +import { useQueryContext } from "@/context/queryContext"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; + +export const useDVpnGateways = () => { + const { client } = useQueryContext(); + const key = "gateways"; + + const query = useQuery({ + ...getGatewaysOptions({ + client, + }), + placeholderData: keepPreviousData, + }); + + return { + key, + query, + }; +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/hooks/useGatewaysTransformed.ts b/nym-node-status-api/nym-node-status-ui/src/hooks/useGatewaysTransformed.ts new file mode 100644 index 0000000000..02c87b46af --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/hooks/useGatewaysTransformed.ts @@ -0,0 +1,47 @@ +import { getGateways } from "@/client/sdk.gen"; +import { useQueryContext } from "@/context/queryContext"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import React from "react"; + +export const useDVpnGatewaysTransformed = () => { + const { client } = useQueryContext(); + const key = "gateways"; + + const queryFn = React.useCallback(async () => { + const { data, error } = await getGateways({ client }); + if (error) throw error; + return (data || []).map((g) => { + const wg = g.last_probe?.outcome.wg as any; + const downloadSpeedMBPerSec = wg + ? Math.round( + (10 * ((wg?.downloaded_file_size_bytes_v4 || 0) / 1024 / 1024)) / + ((wg?.download_duration_milliseconds_v4 || 1) / 1000), + ) / 10 + : undefined; + const downloadSpeedIpv6MBPerSec = wg + ? Math.round( + (10 * ((wg?.downloaded_file_size_bytes_v6 || 0) / 1024 / 1024)) / + ((wg?.download_duration_milliseconds_v6 || 1) / 1000), + ) / 10 + : undefined; + return { + ...g, + extra: { + downloadSpeedMBPerSec, + downloadSpeedIpv6MBPerSec, + }, + }; + }); + }, [client]); + + const query = useQuery({ + queryKey: [key], + queryFn, + placeholderData: keepPreviousData, + }); + + return { + key, + query, + }; +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/hooks/useNymNodes.ts b/nym-node-status-api/nym-node-status-ui/src/hooks/useNymNodes.ts new file mode 100644 index 0000000000..fa17795d7d --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/hooks/useNymNodes.ts @@ -0,0 +1,52 @@ +import type { + DescribedNodeType, + NodeDescription, + NodeGeoData, + NodeRewarding, + NymNodeData, + U32, +} from "@/client"; +import { nymNodesOptions } from "@/client/@tanstack/react-query.gen"; +import { useQueryContext } from "@/context/queryContext"; +import type { Pagination } from "@/hooks/types"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; + +// TODO: how to re-use autogenerated subtype +export type NymNode = { + accepted_tnc: boolean; + bonded: boolean; + bonding_address?: string | null; + description: NodeDescription; + geoip?: null | NodeGeoData; + identity_key: string; + ip_address: string; + node_id: U32; + node_type: DescribedNodeType; + original_pledge: number; + rewarding_details?: null | NodeRewarding; + self_description: NymNodeData; + total_stake: string; + uptime: number; +}; + +export const useNymNodes = (props?: Pagination) => { + const { client } = useQueryContext(); + const { pageIndex = 0, pageSize = 10 } = props || {}; + const key = "nym-nodes"; + + const query = useQuery({ + ...nymNodesOptions({ + client, + query: { + page: pageIndex, + size: pageSize, + }, + }), + placeholderData: keepPreviousData, + }); + + return { + key, + query, + }; +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/layouts/LayoutWithNav.tsx b/nym-node-status-api/nym-node-status-ui/src/layouts/LayoutWithNav.tsx new file mode 100644 index 0000000000..e82ba5336c --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/layouts/LayoutWithNav.tsx @@ -0,0 +1,29 @@ +import Copyright from "@/components/Copyright"; +import AppNavbar from "@/components/nav/AppNavbar"; +import SideMenu from "@/components/nav/SideMenu"; +import Box from "@mui/material/Box"; +import { alpha } from "@mui/material/styles"; +import type React from "react"; + +export default function LayoutWithNav({ + children, +}: { children?: React.ReactNode }) { + return ( + + + + {/* Main content */} + ({ + flexGrow: 1, + backgroundColor: alpha(theme.palette.background.default, 1), + overflow: "auto", + })} + > + {children} + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/layouts/NestedLayoutWithHeader.tsx b/nym-node-status-api/nym-node-status-ui/src/layouts/NestedLayoutWithHeader.tsx new file mode 100644 index 0000000000..9b2e50bc45 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/layouts/NestedLayoutWithHeader.tsx @@ -0,0 +1,23 @@ +import Header from "@/components/nav/Header"; +import Stack from "@mui/material/Stack"; +import type React from "react"; + +export default function NestedLayoutWithHeader({ + children, + header, +}: { children?: React.ReactNode; header?: React.ReactNode }) { + return ( + +
+ {children} + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/theme/ColorModeIconDropdown.tsx b/nym-node-status-api/nym-node-status-ui/src/theme/ColorModeIconDropdown.tsx new file mode 100644 index 0000000000..dd1ebef7d4 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/theme/ColorModeIconDropdown.tsx @@ -0,0 +1,91 @@ +"use client"; + +import DarkModeIcon from "@mui/icons-material/DarkModeRounded"; +import LightModeIcon from "@mui/icons-material/LightModeRounded"; +import Box from "@mui/material/Box"; +import IconButton, { type IconButtonOwnProps } from "@mui/material/IconButton"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import { useColorScheme } from "@mui/material/styles"; +import * as React from "react"; + +export default function ColorModeIconDropdown(props: IconButtonOwnProps) { + const { mode, systemMode, setMode } = useColorScheme(); + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + const handleMode = (targetMode: "system" | "light" | "dark") => () => { + setMode(targetMode); + handleClose(); + }; + if (!mode) { + return ( + ({ + verticalAlign: "bottom", + display: "inline-flex", + width: "2.25rem", + height: "2.25rem", + borderRadius: theme.shape.borderRadius, + border: "1px solid", + borderColor: theme.palette.divider, + })} + /> + ); + } + const resolvedMode = (systemMode || mode) as "light" | "dark"; + const icon = { + light: , + dark: , + }[resolvedMode]; + return ( + + + {icon} + + + + System + + + Light + + + Dark + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/theme/index.tsx b/nym-node-status-api/nym-node-status-ui/src/theme/index.tsx new file mode 100644 index 0000000000..4773499620 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/theme/index.tsx @@ -0,0 +1,27 @@ +import { ThemeProvider, createTheme } from "@mui/material/styles"; +import * as React from "react"; + +interface AppThemeProps { + children: React.ReactNode; + // themeComponents?: ThemeOptions["components"]; +} + +export default function AppTheme(props: AppThemeProps) { + const { children } = props; + const theme = React.useMemo(() => { + return createTheme({ + colorSchemes: { + dark: true, + light: true, + }, + typography: { + fontFamily: "system-ui, sans-serif", + }, + }); + }, []); + return ( + + {children} + + ); +} From f960bfa91b2bbbdbb2531c2e6e348452c7e43cd0 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Mon, 17 Nov 2025 11:40:27 +0100 Subject: [PATCH 091/180] probe fixes testing probe locally --- nym-gateway-probe/src/lib.rs | 4 ++-- nym-gateway-probe/src/nodes.rs | 10 ++++++++++ nym-gateway-probe/src/run.rs | 8 +++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/nym-gateway-probe/src/lib.rs b/nym-gateway-probe/src/lib.rs index 76b8c5c567..2c2f16ab2f 100644 --- a/nym-gateway-probe/src/lib.rs +++ b/nym-gateway-probe/src/lib.rs @@ -186,7 +186,7 @@ impl Probe { ) -> anyhow::Result { let tickets_materials = self.credentials_args.decode_attached_ticket_materials()?; - let tested_entry = self.tested_node.is_same_as_entry(); + let tested_entry = !only_wireguard; let (mixnet_entry_gateway_id, node_info) = self.lookup_gateway(&directory).await?; let storage = Ephemeral::default(); @@ -232,7 +232,7 @@ impl Probe { only_wireguard: bool, min_mixnet_performance: Option, ) -> anyhow::Result { - let tested_entry = self.tested_node.is_same_as_entry(); + let tested_entry = !only_wireguard; let (mixnet_entry_gateway_id, node_info) = self.lookup_gateway(&directory).await?; if config_dir.is_file() { diff --git a/nym-gateway-probe/src/nodes.rs b/nym-gateway-probe/src/nodes.rs index 0726bb3b7d..773470859f 100644 --- a/nym-gateway-probe/src/nodes.rs +++ b/nym-gateway-probe/src/nodes.rs @@ -199,6 +199,16 @@ impl NymApiDirectory { .map(|(id, _)| *id) } + pub fn random_entry_gateway(&self) -> anyhow::Result { + info!("Selecting random entry gateway"); + self.nodes + .iter() + .filter(|(_, n)| n.described.description.declared_role.entry) + .choose(&mut rand::thread_rng()) + .ok_or(anyhow!("no entry gateways available")) + .map(|(id, _)| *id) + } + pub fn get_nym_node(&self, identity: NodeIdentity) -> anyhow::Result { self.nodes .get(&identity) diff --git a/nym-gateway-probe/src/run.rs b/nym-gateway-probe/src/run.rs index 9487b10905..064a0b49ef 100644 --- a/nym-gateway-probe/src/run.rs +++ b/nym-gateway-probe/src/run.rs @@ -126,8 +126,14 @@ pub(crate) async fn run() -> anyhow::Result { let entry = if let Some(gateway) = &args.entry_gateway { NodeIdentity::from_base58_string(gateway)? + } else if let Some(node) = args.node { + if directory.entry_gateway(&node).is_ok() { + node + } else { + directory.random_entry_gateway()? + } } else { - directory.random_exit_with_ipr()? + directory.random_entry_gateway()? }; let test_point = if let Some(node) = args.node { From bcce854a8b0c76b58cbaf8f063191b64428bb78e Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Mon, 17 Nov 2025 16:03:28 +0100 Subject: [PATCH 092/180] more attempts --- nym-gateway-probe/src/lib.rs | 46 ++++++++++++++++++++++++++---------- nym-gateway-probe/src/run.rs | 33 ++++++++++++++++---------- 2 files changed, 55 insertions(+), 24 deletions(-) diff --git a/nym-gateway-probe/src/lib.rs b/nym-gateway-probe/src/lib.rs index 2c2f16ab2f..5f7827dff0 100644 --- a/nym-gateway-probe/src/lib.rs +++ b/nym-gateway-probe/src/lib.rs @@ -130,12 +130,20 @@ pub enum TestedNode { SameAsEntry, Custom { identity: NodeIdentity, + shares_entry: bool, }, } impl TestedNode { pub fn is_same_as_entry(&self) -> bool { - matches!(self, TestedNode::SameAsEntry) + matches!( + self, + TestedNode::SameAsEntry + | TestedNode::Custom { + shares_entry: true, + .. + } + ) } } @@ -186,7 +194,7 @@ impl Probe { ) -> anyhow::Result { let tickets_materials = self.credentials_args.decode_attached_ticket_materials()?; - let tested_entry = !only_wireguard; + let tested_entry = self.tested_node.is_same_as_entry(); let (mixnet_entry_gateway_id, node_info) = self.lookup_gateway(&directory).await?; let storage = Ephemeral::default(); @@ -232,7 +240,7 @@ impl Probe { only_wireguard: bool, min_mixnet_performance: Option, ) -> anyhow::Result { - let tested_entry = !only_wireguard; + let tested_entry = self.tested_node.is_same_as_entry(); let (mixnet_entry_gateway_id, node_info) = self.lookup_gateway(&directory).await?; if config_dir.is_file() { @@ -309,7 +317,20 @@ impl Probe { let entry_gateway = directory.entry_gateway(&self.entrypoint)?; let node_info: TestedNodeDetails = match self.tested_node { - TestedNode::Custom { identity } => { + TestedNode::Custom { + identity: _, + shares_entry: true, + } => { + debug!( + "testing node {} as both entry and exit", + entry_gateway.identity() + ); + entry_gateway.to_testable_node()? + } + TestedNode::Custom { + identity, + shares_entry: false, + } => { let node = directory.get_nym_node(identity)?; info!( "testing node {} (via entry {})", @@ -421,12 +442,17 @@ impl Probe { storage.credential_store().clone(), client, ); - let credential = bw_controller - .prepare_ecash_ticket( + let (wg_ticket_type, credential_provider) = if tested_entry { + ( TicketType::V1WireguardEntry, nym_address.gateway().to_bytes(), - 1, ) + } else { + (TicketType::V1WireguardExit, node_info.identity.to_bytes()) + }; + + let credential = bw_controller + .prepare_ecash_ticket(wg_ticket_type, credential_provider, 1) .await? .data; @@ -711,11 +737,7 @@ async fn do_ping_entry( } info!("Successfully mixnet pinged ourselves"); - if tested_entry { - Entry::success() - } else { - Entry::NotTested - } + Entry::success() } async fn connect_exit( diff --git a/nym-gateway-probe/src/run.rs b/nym-gateway-probe/src/run.rs index 064a0b49ef..9c715bcdda 100644 --- a/nym-gateway-probe/src/run.rs +++ b/nym-gateway-probe/src/run.rs @@ -124,22 +124,31 @@ pub(crate) async fn run() -> anyhow::Result { let directory = NymApiDirectory::new(api_url).await?; - let entry = if let Some(gateway) = &args.entry_gateway { - NodeIdentity::from_base58_string(gateway)? - } else if let Some(node) = args.node { - if directory.entry_gateway(&node).is_ok() { - node - } else { - directory.random_entry_gateway()? - } + let node_override = args.node; + let entry_override = if let Some(gateway) = &args.entry_gateway { + Some(NodeIdentity::from_base58_string(gateway)?) + } else { + None + }; + + let entry = if let Some(entry) = entry_override { + entry + } else if let Some(node) = node_override { + node } else { directory.random_entry_gateway()? }; - let test_point = if let Some(node) = args.node { - TestedNode::Custom { identity: node } - } else { - TestedNode::SameAsEntry + let test_point = match (node_override, entry_override) { + (Some(node), Some(_)) => TestedNode::Custom { + identity: node, + shares_entry: false, + }, + (Some(node), None) => TestedNode::Custom { + identity: node, + shares_entry: true, + }, + (None, _) => TestedNode::SameAsEntry, }; let mut trial = From ef25480c201048ae031f29dc269daaf788f8be2b Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Mon, 17 Nov 2025 17:05:34 +0100 Subject: [PATCH 093/180] fix --- .../nym-node-setup/network-tunnel-manager.sh | 53 +++++++++---------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index acd0186366..71e31e5c9f 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -296,20 +296,20 @@ apply_iptables_rules() { iptables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE iptables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null || \ - iptables -A FORWARD -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT + iptables -I FORWARD 1 -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT iptables -C FORWARD -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ - iptables -A FORWARD -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT + iptables -I FORWARD 2 -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT # ipv6 nat and forwarding ip6tables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null || \ ip6tables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE ip6tables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null || \ - ip6tables -A FORWARD -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT + ip6tables -I FORWARD 1 -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT ip6tables -C FORWARD -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ - ip6tables -A FORWARD -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT + ip6tables -I FORWARD 2 -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT save_iptables_rules } @@ -507,45 +507,40 @@ setup_nat_rules() { fi if ! iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null; then - iptables -A FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT + iptables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT fi if ! iptables -C FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then - iptables -A FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT + iptables -I FORWARD 2 -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT fi if ! ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null; then - ip6tables -A FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT + ip6tables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -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 -A FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT + ip6tables -I FORWARD 2 -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT fi } configure_exit_dns_and_icmp() { echo "ensuring dns and icmp are allowed inside nym exit chain" - if ! iptables -C "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT 2>/dev/null; then - iptables -A "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT - fi - if ! iptables -C "$NYM_CHAIN" -p tcp --dport 53 -j ACCEPT 2>/dev/null; then - iptables -A "$NYM_CHAIN" -p tcp --dport 53 -j ACCEPT - fi - if ! ip6tables -C "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT 2>/dev/null; then - ip6tables -A "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT - fi - if ! ip6tables -C "$NYM_CHAIN" -p tcp --dport 53 -j ACCEPT 2>/dev/null; then - ip6tables -A "$NYM_CHAIN" -p tcp --dport 53 -j ACCEPT - fi + # Remove any existing DNS/ICMP rules first to avoid duplicates + iptables -D "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT 2>/dev/null || true + iptables -D "$NYM_CHAIN" -p tcp --dport 53 -j ACCEPT 2>/dev/null || true + iptables -D "$NYM_CHAIN" -p icmp --icmp-type echo-request -j ACCEPT 2>/dev/null || true + iptables -D "$NYM_CHAIN" -p icmp --icmp-type echo-reply -j ACCEPT 2>/dev/null || true + ip6tables -D "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT 2>/dev/null || true + ip6tables -D "$NYM_CHAIN" -p tcp --dport 53 -j ACCEPT 2>/dev/null || true + ip6tables -D "$NYM_CHAIN" -p ipv6-icmp -j ACCEPT 2>/dev/null || true - if ! iptables -C "$NYM_CHAIN" -p icmp --icmp-type echo-request -j ACCEPT 2>/dev/null; then - iptables -A "$NYM_CHAIN" -p icmp --icmp-type echo-request -j ACCEPT - fi - if ! iptables -C "$NYM_CHAIN" -p icmp --icmp-type echo-reply -j ACCEPT 2>/dev/null; then - iptables -A "$NYM_CHAIN" -p icmp --icmp-type echo-reply -j ACCEPT - fi - if ! ip6tables -C "$NYM_CHAIN" -p ipv6-icmp -j ACCEPT 2>/dev/null; then - ip6tables -A "$NYM_CHAIN" -p ipv6-icmp -j ACCEPT - fi + # Insert rules at the beginning in correct order: DNS first, then ICMP + iptables -I "$NYM_CHAIN" 1 -p udp --dport 53 -j ACCEPT + iptables -I "$NYM_CHAIN" 2 -p tcp --dport 53 -j ACCEPT + iptables -I "$NYM_CHAIN" 3 -p icmp --icmp-type echo-request -j ACCEPT + iptables -I "$NYM_CHAIN" 4 -p icmp --icmp-type echo-reply -j ACCEPT + ip6tables -I "$NYM_CHAIN" 1 -p udp --dport 53 -j ACCEPT + ip6tables -I "$NYM_CHAIN" 2 -p tcp --dport 53 -j ACCEPT + ip6tables -I "$NYM_CHAIN" 3 -p ipv6-icmp -j ACCEPT } apply_port_allowlist() { From 82a9563ca0cbe7c7441abf8699f44750f9b14322 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Tue, 18 Nov 2025 13:12:35 +0100 Subject: [PATCH 094/180] add a checker script --- .../nym-node-setup/check-firewall-setup.sh | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 scripts/nym-node-setup/check-firewall-setup.sh diff --git a/scripts/nym-node-setup/check-firewall-setup.sh b/scripts/nym-node-setup/check-firewall-setup.sh new file mode 100644 index 0000000000..713750b65e --- /dev/null +++ b/scripts/nym-node-setup/check-firewall-setup.sh @@ -0,0 +1,99 @@ +#!/bin/bash +# helper script to verify firewall ordering +set -euo pipefail + +bold() { printf '\033[1m%s\033[0m\n' "$*"; } +warn() { printf '\033[31m⚠ %s\033[0m\n' "$*"; } +ok() { printf '\033[32m✓ %s\033[0m\n' "$*"; } + +RULE_OFFSET=2 # this is because thefirst rule appears on line 3 + +get_rule_line() { + local chain=$1 + local rule_idx=$2 + iptables -L "$chain" -n --line-numbers | sed -n "$((rule_idx + RULE_OFFSET))p" +} + +check_forward_chain() { + local output + output=$(iptables -L FORWARD -n --line-numbers) + + if echo "$output" | grep -q "^1[[:space:]]\+NYM-EXIT"; then + ok "FORWARD rule 1 jumps to NYM-EXIT" + else + warn "FORWARD rule 1 is not NYM-EXIT; re-run network-tunnel-manager.sh exit_policy_install" + return 1 + fi + + if echo "$output" | grep -q "ACCEPT.*state RELATED,ESTABLISHED"; then + ok "FORWARD chain contains RELATED,ESTABLISHED accepts (WG return path)" + else + warn "FORWARD chain missing RELATED,ESTABLISHED accepts; re-run network-tunnel-manager.sh apply_iptables_rules_wg" + return 1 + fi + + return 0 +} + +check_nym_exit_chain() { + local errors=0 + local patterns=( + "udp.*dpt:53" + "tcp.*dpt:53" + "icmp.*type 8" + "icmp.*type 0" + ) + + for idx in "${!patterns[@]}"; do + local rule_no=$((idx + 1)) + local line + line=$(get_rule_line "NYM-EXIT" "$rule_no") + if [[ "$line" =~ ${patterns[$idx]} ]]; then + ok "NYM-EXIT rule $rule_no matches ${patterns[$idx]}" + else + warn "NYM-EXIT rule $rule_no is not ${patterns[$idx]}; re-run network-tunnel-manager.sh exit_policy_install" + errors=1 + fi + done + + local last_rule + last_rule=$(iptables -L NYM-EXIT -n --line-numbers | awk 'NR>2 {line=$0} END {print line}') + if [[ -z "${last_rule:-}" ]]; then + warn "NYM-EXIT chain is empty; re-run network-tunnel-manager.sh exit_policy_install" + errors=1 + elif [[ "$last_rule" =~ REJECT ]] && [[ "$last_rule" =~ 0\.0\.0\.0/0 ]]; then + ok "NYM-EXIT ends with REJECT all" + else + warn "NYM-EXIT final rule is not a REJECT all (got: $last_rule)" + errors=1 + fi + + return $errors +} + +main() { + bold "Checking IPv4 firewall ordering…" + local errors=0 + check_forward_chain || errors=1 + check_nym_exit_chain || errors=1 + + if command -v ip6tables >/dev/null 2>&1; then + bold "Checking IPv6 firewall ordering…" + if ip6tables -L NYM-EXIT -n --line-numbers >/dev/null 2>&1; then + if ! ip6tables -L NYM-EXIT -n --line-numbers | sed -n '3p' | grep -q "udp.*dpt:53"; then + warn "ip6tables NYM-EXIT rule 1 is not UDP 53" + errors=1 + fi + fi + fi + + if [[ $errors -ne 0 ]]; then + warn "There may be some problems, it's recommended to re-run network-tunnel-manager.sh exit_policy_install after configuring UFW." + exit 1 + else + ok "It's looking good!" + fi +} + +main "$@" + From b742ace7e0f7278f538598fa667841f0dcd3f464 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Tue, 18 Nov 2025 14:24:19 +0100 Subject: [PATCH 095/180] add firewall check to the main script --- .../nym-node-setup/check-firewall-setup.sh | 99 ------------------- .../nym-node-setup/network-tunnel-manager.sh | 97 +++++++++++++++++- 2 files changed, 95 insertions(+), 101 deletions(-) delete mode 100644 scripts/nym-node-setup/check-firewall-setup.sh diff --git a/scripts/nym-node-setup/check-firewall-setup.sh b/scripts/nym-node-setup/check-firewall-setup.sh deleted file mode 100644 index 713750b65e..0000000000 --- a/scripts/nym-node-setup/check-firewall-setup.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash -# helper script to verify firewall ordering -set -euo pipefail - -bold() { printf '\033[1m%s\033[0m\n' "$*"; } -warn() { printf '\033[31m⚠ %s\033[0m\n' "$*"; } -ok() { printf '\033[32m✓ %s\033[0m\n' "$*"; } - -RULE_OFFSET=2 # this is because thefirst rule appears on line 3 - -get_rule_line() { - local chain=$1 - local rule_idx=$2 - iptables -L "$chain" -n --line-numbers | sed -n "$((rule_idx + RULE_OFFSET))p" -} - -check_forward_chain() { - local output - output=$(iptables -L FORWARD -n --line-numbers) - - if echo "$output" | grep -q "^1[[:space:]]\+NYM-EXIT"; then - ok "FORWARD rule 1 jumps to NYM-EXIT" - else - warn "FORWARD rule 1 is not NYM-EXIT; re-run network-tunnel-manager.sh exit_policy_install" - return 1 - fi - - if echo "$output" | grep -q "ACCEPT.*state RELATED,ESTABLISHED"; then - ok "FORWARD chain contains RELATED,ESTABLISHED accepts (WG return path)" - else - warn "FORWARD chain missing RELATED,ESTABLISHED accepts; re-run network-tunnel-manager.sh apply_iptables_rules_wg" - return 1 - fi - - return 0 -} - -check_nym_exit_chain() { - local errors=0 - local patterns=( - "udp.*dpt:53" - "tcp.*dpt:53" - "icmp.*type 8" - "icmp.*type 0" - ) - - for idx in "${!patterns[@]}"; do - local rule_no=$((idx + 1)) - local line - line=$(get_rule_line "NYM-EXIT" "$rule_no") - if [[ "$line" =~ ${patterns[$idx]} ]]; then - ok "NYM-EXIT rule $rule_no matches ${patterns[$idx]}" - else - warn "NYM-EXIT rule $rule_no is not ${patterns[$idx]}; re-run network-tunnel-manager.sh exit_policy_install" - errors=1 - fi - done - - local last_rule - last_rule=$(iptables -L NYM-EXIT -n --line-numbers | awk 'NR>2 {line=$0} END {print line}') - if [[ -z "${last_rule:-}" ]]; then - warn "NYM-EXIT chain is empty; re-run network-tunnel-manager.sh exit_policy_install" - errors=1 - elif [[ "$last_rule" =~ REJECT ]] && [[ "$last_rule" =~ 0\.0\.0\.0/0 ]]; then - ok "NYM-EXIT ends with REJECT all" - else - warn "NYM-EXIT final rule is not a REJECT all (got: $last_rule)" - errors=1 - fi - - return $errors -} - -main() { - bold "Checking IPv4 firewall ordering…" - local errors=0 - check_forward_chain || errors=1 - check_nym_exit_chain || errors=1 - - if command -v ip6tables >/dev/null 2>&1; then - bold "Checking IPv6 firewall ordering…" - if ip6tables -L NYM-EXIT -n --line-numbers >/dev/null 2>&1; then - if ! ip6tables -L NYM-EXIT -n --line-numbers | sed -n '3p' | grep -q "udp.*dpt:53"; then - warn "ip6tables NYM-EXIT rule 1 is not UDP 53" - errors=1 - fi - fi - fi - - if [[ $errors -ne 0 ]]; then - warn "There may be some problems, it's recommended to re-run network-tunnel-manager.sh exit_policy_install after configuring UFW." - exit 1 - else - ok "It's looking good!" - fi -} - -main "$@" - diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 71e31e5c9f..883c068f4b 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -800,8 +800,95 @@ test_exit_policy_connectivity() { echo "connectivity tests finished" } + ############################################################################### -# part 3: exit policy verification tests +# part 3: check the firewall setup +############################################################################### + +firewall_rule_line() { + local chain=$1 + local rule_idx=$2 + # this is because thefirst rule appears on line 3 + iptables -L "$chain" -n --line-numbers | sed -n "$((rule_idx + 2))p" +} + +check_forward_chain() { + local output + output=$(iptables -L FORWARD -n --line-numbers) + + if ! echo "$output" | grep -q "^1[[:space:]]\+$NYM_CHAIN"; then + echo "FORWARD rule 1 is not ${NYM_CHAIN}; re-run network-tunnel-manager.sh exit_policy_install" + return 1 + fi + + if ! echo "$output" | grep -q "ACCEPT.*state RELATED,ESTABLISHED"; then + echo "FORWARD chain missing RELATED,ESTABLISHED accepts; re-run network-tunnel-manager.sh apply_iptables_rules_wg" + return 1 + fi + + echo "FORWARD chain ordering looks good" + return 0 +} + +check_nym_exit_chain() { + local errors=0 + local patterns=("udp.*dpt:53" "tcp.*dpt:53" "icmp.*type 8" "icmp.*type 0") + + for idx in "${!patterns[@]}"; do + local line + line=$(firewall_rule_line "$NYM_CHAIN" $((idx + 1))) + if [[ "$line" =~ ${patterns[$idx]} ]]; then + echo "${NYM_CHAIN} rule $((idx + 1)) ok (${patterns[$idx]})" + else + echo "${NYM_CHAIN} rule $((idx + 1)) is not ${patterns[$idx]}; re-run network-tunnel-manager.sh exit_policy_install" + errors=1 + fi + done + + local last_rule + last_rule=$(iptables -L "$NYM_CHAIN" -n --line-numbers | awk 'NR>2 {line=$0} END {print line}') + if [[ -z "${last_rule:-}" ]]; then + echo "${NYM_CHAIN} chain is empty; re-run network-tunnel-manager.sh exit_policy_install" + errors=1 + elif [[ "$last_rule" =~ REJECT ]] && [[ "$last_rule" =~ 0\.0\.0\.0/0 ]]; then + echo "${NYM_CHAIN} ends with the catch-all REJECT" + else + echo "${NYM_CHAIN} final rule is not the catch-all REJECT (got: $last_rule)" + errors=1 + fi + + return $errors +} + +check_firewall_setup() { + echo "checking ipv4 firewall ordering…" + local errors=0 + + check_forward_chain || errors=1 + check_nym_exit_chain || errors=1 + + if command -v ip6tables >/dev/null 2>&1; then + echo "checking ipv6 firewall ordering…" + if ip6tables -L "$NYM_CHAIN" -n --line-numbers >/dev/null 2>&1; then + if ! ip6tables -L "$NYM_CHAIN" -n --line-numbers | sed -n '3p' | grep -q "udp.*dpt:53"; then + echo "ip6tables ${NYM_CHAIN} rule 1 is not UDP 53" + errors=1 + fi + fi + fi + + if [[ $errors -ne 0 ]]; then + echo "There may be some ordering issues, it is recommended to re-run network-tunnel-manager.sh exit_policy_install after configuring UFW." + return 1 + fi + + echo "It's looking good!" + return 0 +} + + +############################################################################### +# part 4: full exit policy verification tests ############################################################################### test_port_range_rules() { @@ -945,7 +1032,7 @@ exit_policy_run_tests() { } ############################################################################### -# part 4: high level workflows +# part 5: high level workflows ############################################################################### full_tunnel_setup() { @@ -992,6 +1079,7 @@ complete_networking_configuration() { full_tunnel_setup exit_policy_install + check_firewall_setup || echo "firewall order checks reported problems, please review output" exit_policy_run_tests || echo "exit policy tests reported problems, please review output" echo "complete networking configuration finished" @@ -1080,6 +1168,10 @@ case "$cmd" in show_exit_policy_status status=$? ;; + check_firewall_setup) + check_firewall_setup + status=$? + ;; exit_policy_test_connectivity) test_exit_policy_connectivity status=$? @@ -1121,6 +1213,7 @@ tunnel and nat helpers: exit policy manager: exit_policy_install install exit policy (iptables rules and blocklist) + check_firewall_setup run ordering sanity check (dns/icmp + FORWARD jump) exit_policy_status show status of exit policy and forwarding exit_policy_test_connectivity test connectivity via ${WG_INTERFACE} exit_policy_clear remove ${NYM_CHAIN} chains and hooks From bdc0f5022d0cbf8553be30a55c76c4eadafc3efa Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Tue, 18 Nov 2025 16:38:01 +0100 Subject: [PATCH 096/180] / move ensure_jq where needed --- .../nym-node-setup/network-tunnel-manager.sh | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index acd0186366..7017adbd4d 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -12,24 +12,6 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi -echo "checking for jq..." - -if command -v jq >/dev/null 2>&1; then - echo "jq is already installed" - # continue script execution -else - echo "jq not found, installing..." - apt-get update -y - DEBIAN_FRONTEND=noninteractive apt-get install -y jq - - if command -v jq >/dev/null 2>&1; then - echo "jq installed successfully" - else - echo "failed to install jq" - exit 1 - fi -fi - ############################################################################### # basic config ############################################################################### @@ -51,7 +33,6 @@ detect_uplink_interface() { fi } - # uplink device detection, can be overridden NETWORK_DEVICE="${NETWORK_DEVICE:-}" if [[ -z "$NETWORK_DEVICE" ]]; then @@ -79,6 +60,25 @@ NC='\033[0m' # shared helpers ############################################################################### +ensure_jq() { + echo "checking for jq..." + + if command -v jq >/dev/null 2>&1; then + echo "jq is already installed" + else + echo "jq not found, installing..." + apt-get update -y + DEBIAN_FRONTEND=noninteractive apt-get install -y jq + + if command -v jq >/dev/null 2>&1; then + echo "jq installed successfully" + else + echo "failed to install jq" + exit 1 + fi + fi +} + install_iptables_persistent() { if ! dpkg -s iptables-persistent >/dev/null 2>&1; then echo -e "${YELLOW}installing iptables-persistent${NC}" @@ -283,9 +283,6 @@ remove_duplicate_rules() { echo "duplicate rule scan completed for $interface" } - - - apply_iptables_rules() { local interface=$1 echo "applying iptables rules for $interface using uplink $NETWORK_DEVICE" @@ -349,6 +346,7 @@ perform_pings() { } joke_through_tunnel() { + ensure_jq local interface=$1 local green="\033[0;32m" local reset="\033[0m" @@ -905,6 +903,7 @@ test_forward_chain_hook() { test_default_reject_rule() { echo "testing default reject rule at end of ${NYM_CHAIN}" + # not sure this will really check that it is on end if iptables -L "$NYM_CHAIN" | grep -q "REJECT"; then echo "default reject present in ipv4 chain" else From 06c0c36c671bc4b5225204cdabdc23e5c9715914 Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Tue, 18 Nov 2025 17:04:24 +0100 Subject: [PATCH 097/180] + add output for no rules were deduplicated --- scripts/nym-node-setup/network-tunnel-manager.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 7017adbd4d..e7a291bb29 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -207,6 +207,8 @@ remove_duplicate_rules() { echo "warning: could not reliably match ipv4 duplicate rule: $rule" fi done + else + echo "no ipv4 rules were deduplicated" fi done < "$tmp4" @@ -270,6 +272,8 @@ remove_duplicate_rules() { echo "warning: could not match ipv6 duplicate rule reliably: $rule" fi done + else + echo "no ipv6 rules were deduplicated" fi done < "$tmp6" From 2e059865a14d0b0309980f2b2994efdf28ca1cc3 Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Tue, 18 Nov 2025 17:08:12 +0100 Subject: [PATCH 098/180] Revert "/ move ensure_jq where needed" This reverts commit bdc0f5022d0cbf8553be30a55c76c4eadafc3efa. --- .../nym-node-setup/network-tunnel-manager.sh | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index e7a291bb29..01d73705a8 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -12,6 +12,24 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi +echo "checking for jq..." + +if command -v jq >/dev/null 2>&1; then + echo "jq is already installed" + # continue script execution +else + echo "jq not found, installing..." + apt-get update -y + DEBIAN_FRONTEND=noninteractive apt-get install -y jq + + if command -v jq >/dev/null 2>&1; then + echo "jq installed successfully" + else + echo "failed to install jq" + exit 1 + fi +fi + ############################################################################### # basic config ############################################################################### @@ -33,6 +51,7 @@ detect_uplink_interface() { fi } + # uplink device detection, can be overridden NETWORK_DEVICE="${NETWORK_DEVICE:-}" if [[ -z "$NETWORK_DEVICE" ]]; then @@ -60,25 +79,6 @@ NC='\033[0m' # shared helpers ############################################################################### -ensure_jq() { - echo "checking for jq..." - - if command -v jq >/dev/null 2>&1; then - echo "jq is already installed" - else - echo "jq not found, installing..." - apt-get update -y - DEBIAN_FRONTEND=noninteractive apt-get install -y jq - - if command -v jq >/dev/null 2>&1; then - echo "jq installed successfully" - else - echo "failed to install jq" - exit 1 - fi - fi -} - install_iptables_persistent() { if ! dpkg -s iptables-persistent >/dev/null 2>&1; then echo -e "${YELLOW}installing iptables-persistent${NC}" @@ -287,6 +287,9 @@ remove_duplicate_rules() { echo "duplicate rule scan completed for $interface" } + + + apply_iptables_rules() { local interface=$1 echo "applying iptables rules for $interface using uplink $NETWORK_DEVICE" @@ -350,7 +353,6 @@ perform_pings() { } joke_through_tunnel() { - ensure_jq local interface=$1 local green="\033[0;32m" local reset="\033[0m" @@ -907,7 +909,6 @@ test_forward_chain_hook() { test_default_reject_rule() { echo "testing default reject rule at end of ${NYM_CHAIN}" - # not sure this will really check that it is on end if iptables -L "$NYM_CHAIN" | grep -q "REJECT"; then echo "default reject present in ipv4 chain" else From 2933732225b71c5c56e83888ba82043e2c0caf7d Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Tue, 18 Nov 2025 17:08:51 +0100 Subject: [PATCH 099/180] Revert "+ add output for no rules were deduplicated" This reverts commit 06c0c36c671bc4b5225204cdabdc23e5c9715914. --- scripts/nym-node-setup/network-tunnel-manager.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 01d73705a8..acd0186366 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -207,8 +207,6 @@ remove_duplicate_rules() { echo "warning: could not reliably match ipv4 duplicate rule: $rule" fi done - else - echo "no ipv4 rules were deduplicated" fi done < "$tmp4" @@ -272,8 +270,6 @@ remove_duplicate_rules() { echo "warning: could not match ipv6 duplicate rule reliably: $rule" fi done - else - echo "no ipv6 rules were deduplicated" fi done < "$tmp6" From bfcb4c79bb0ee6cd03efca62d570cc3cbd0ae599 Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Wed, 19 Nov 2025 13:07:08 +0100 Subject: [PATCH 100/180] / fix test_default_reject_rule --- .../nym-node-setup/network-tunnel-manager.sh | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index acd0186366..266ed03679 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -903,14 +903,23 @@ test_forward_chain_hook() { } test_default_reject_rule() { - echo "testing default reject rule at end of ${NYM_CHAIN}" + echo "testing default reject rule position in ${NYM_CHAIN}" - if iptables -L "$NYM_CHAIN" | grep -q "REJECT"; then - echo "default reject present in ipv4 chain" - else - echo "default reject missing in ipv4 chain" + local last_rule_v4 + last_rule_v4=$(iptables -S "$NYM_CHAIN" | awk '/^-A /{rule=$0} END{print rule}') + if [[ "$last_rule_v4" != "-A $NYM_CHAIN -j REJECT --reject-with icmp-port-unreachable" ]]; then + echo "default reject missing or not last in ipv4 chain" return 1 fi + + local last_rule_v6 + last_rule_v6=$(ip6tables -S "$NYM_CHAIN" | awk '/^-A /{rule=$0} END{print rule}') + if [[ "$last_rule_v6" != "-A $NYM_CHAIN -j REJECT --reject-with icmp6-port-unreachable" ]]; then + echo "default reject missing or not last in ipv6 chain" + return 1 + fi + + echo "default reject confirmed at end of ${NYM_CHAIN}" } exit_policy_run_tests() { From c23e139eaab8c68ed700a95b20bfed0f6b998cc0 Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Wed, 19 Nov 2025 13:11:05 +0100 Subject: [PATCH 101/180] + COLORS test_default_reject_rule --- scripts/nym-node-setup/network-tunnel-manager.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 266ed03679..ade9c1836e 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -903,23 +903,23 @@ test_forward_chain_hook() { } test_default_reject_rule() { - echo "testing default reject rule position in ${NYM_CHAIN}" + echo -e "${YELLOW}testing default reject rule position in ${NYM_CHAIN}${NC}" local last_rule_v4 last_rule_v4=$(iptables -S "$NYM_CHAIN" | awk '/^-A /{rule=$0} END{print rule}') if [[ "$last_rule_v4" != "-A $NYM_CHAIN -j REJECT --reject-with icmp-port-unreachable" ]]; then - echo "default reject missing or not last in ipv4 chain" + echo -e "${RED}default reject missing or not last in ipv4 chain${NC}" return 1 fi local last_rule_v6 last_rule_v6=$(ip6tables -S "$NYM_CHAIN" | awk '/^-A /{rule=$0} END{print rule}') if [[ "$last_rule_v6" != "-A $NYM_CHAIN -j REJECT --reject-with icmp6-port-unreachable" ]]; then - echo "default reject missing or not last in ipv6 chain" + echo -e "${RED}default reject missing or not last in ipv6 chain${NC}" return 1 fi - echo "default reject confirmed at end of ${NYM_CHAIN}" + echo -e "${GREEN}default reject confirmed at end of ${NYM_CHAIN}${NC}" } exit_policy_run_tests() { From 4736f1eb527a618b3c7dae95f7d49e85ab1acc6a Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Wed, 19 Nov 2025 13:26:14 +0100 Subject: [PATCH 102/180] / fix login in exit_policy_run_tests --- scripts/nym-node-setup/network-tunnel-manager.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index ade9c1836e..b1c637e51c 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -934,17 +934,17 @@ exit_policy_run_tests() { local total=0 local failed=0 - test_forward_chain_hook || ((failed++)) + test_forward_chain_hook || ((failed += 1)) ((total++)) - test_port_range_rules || ((failed++)) + test_port_range_rules || ((failed += 1)) ((total++)) - test_critical_services || ((failed++)) + test_critical_services || ((failed += 1)) ((total++)) if [[ $skip_default -eq 0 ]]; then - test_default_reject_rule || ((failed++)) + test_default_reject_rule || ((failed += 1)) ((total++)) fi From 95ee3a7c7d3ea78bd1b9dec1bd8dc7ee895621e2 Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Wed, 19 Nov 2025 13:34:35 +0100 Subject: [PATCH 103/180] + colors test_forward_chain_hook & complete_networking_configuration --- scripts/nym-node-setup/network-tunnel-manager.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index b1c637e51c..e08aac84cc 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -881,21 +881,21 @@ test_critical_services() { } test_forward_chain_hook() { - echo "testing forward chain hook direction for ${NYM_CHAIN}" + echo -e "${YELLOW}testing forward chain hook direction for ${NYM_CHAIN}${NC}" local failures=0 if iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then - echo "ipv4 forward hook ok: -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN" + echo echo -e "${GREEN}ipv4 forward hook ok: -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN${NC}" else - echo "ipv4 forward hook missing or wrong" + echo -e "${RED}ipv4 forward hook missing or wrong${NC}" ((failures++)) fi if ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then - echo "ipv6 forward hook ok: -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN" + echo echo -e "${GREEN}ipv6 forward hook ok: -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN${NC}" else - echo "ipv6 forward hook missing or wrong" + echo -e "${RED}ipv6 forward hook missing or wrong${NC}" ((failures++)) fi @@ -1002,11 +1002,11 @@ exit_policy_install() { } complete_networking_configuration() { - echo "starting complete networking configuration: tunnels + exit policy" + echo -e "${YELLOW}starting complete networking configuration: tunnels + exit policy${NC}" full_tunnel_setup exit_policy_install - exit_policy_run_tests || echo "exit policy tests reported problems, please review output" + exit_policy_run_tests || echo -e "${RED}exit policy tests reported problems, please review output${NC}" echo "complete networking configuration finished" } From 40a7a87c615a2412f3b7058d646e9c92f968af01 Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Wed, 19 Nov 2025 13:36:41 +0100 Subject: [PATCH 104/180] + colorl jq install --- scripts/nym-node-setup/network-tunnel-manager.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index e08aac84cc..8413af1742 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -15,17 +15,17 @@ fi echo "checking for jq..." if command -v jq >/dev/null 2>&1; then - echo "jq is already installed" + echo -e "${GREEN}jq is already installed${NC}" # continue script execution else - echo "jq not found, installing..." + echo -e "${YELLOW}jq not found, installing...${NC}" apt-get update -y DEBIAN_FRONTEND=noninteractive apt-get install -y jq if command -v jq >/dev/null 2>&1; then - echo "jq installed successfully" + echo -e "${GREEN}jq installed successfully${NC}" else - echo "failed to install jq" + echo -e "${RED}failed to install jq${NC}" exit 1 fi fi From 8de37eb6c91fa548ef2c0c463f9d626ed05a9949 Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Wed, 19 Nov 2025 13:43:24 +0100 Subject: [PATCH 105/180] / move ensure_jq where needed --- .../nym-node-setup/network-tunnel-manager.sh | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 8413af1742..799ee4f6d2 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -12,24 +12,6 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi -echo "checking for jq..." - -if command -v jq >/dev/null 2>&1; then - echo -e "${GREEN}jq is already installed${NC}" - # continue script execution -else - echo -e "${YELLOW}jq not found, installing...${NC}" - apt-get update -y - DEBIAN_FRONTEND=noninteractive apt-get install -y jq - - if command -v jq >/dev/null 2>&1; then - echo -e "${GREEN}jq installed successfully${NC}" - else - echo -e "${RED}failed to install jq${NC}" - exit 1 - fi -fi - ############################################################################### # basic config ############################################################################### @@ -51,7 +33,6 @@ detect_uplink_interface() { fi } - # uplink device detection, can be overridden NETWORK_DEVICE="${NETWORK_DEVICE:-}" if [[ -z "$NETWORK_DEVICE" ]]; then @@ -79,6 +60,25 @@ NC='\033[0m' # shared helpers ############################################################################### +ensure_jq() { + echo "checking for jq..." + + if command -v jq >/dev/null 2>&1; then + echo "jq is already installed" + else + echo "jq not found, installing..." + apt-get update -y + DEBIAN_FRONTEND=noninteractive apt-get install -y jq + + if command -v jq >/dev/null 2>&1; then + echo "jq installed successfully" + else + echo "failed to install jq" + exit 1 + fi + fi +} + install_iptables_persistent() { if ! dpkg -s iptables-persistent >/dev/null 2>&1; then echo -e "${YELLOW}installing iptables-persistent${NC}" @@ -283,9 +283,6 @@ remove_duplicate_rules() { echo "duplicate rule scan completed for $interface" } - - - apply_iptables_rules() { local interface=$1 echo "applying iptables rules for $interface using uplink $NETWORK_DEVICE" @@ -349,6 +346,7 @@ perform_pings() { } joke_through_tunnel() { + ensure_jq local interface=$1 local green="\033[0;32m" local reset="\033[0m" @@ -905,6 +903,11 @@ test_forward_chain_hook() { test_default_reject_rule() { echo -e "${YELLOW}testing default reject rule position in ${NYM_CHAIN}${NC}" + # not sure this will really check that it is on end + if iptables -L "$NYM_CHAIN" | grep -q "REJECT"; then + echo "default reject present in ipv4 chain" + else + echo "default reject missing in ipv4 chain" local last_rule_v4 last_rule_v4=$(iptables -S "$NYM_CHAIN" | awk '/^-A /{rule=$0} END{print rule}') if [[ "$last_rule_v4" != "-A $NYM_CHAIN -j REJECT --reject-with icmp-port-unreachable" ]]; then From 5496cce5c967acc211474f9db41e26098cdd774f Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Wed, 19 Nov 2025 13:48:36 +0100 Subject: [PATCH 106/180] / move color definition --- .../nym-node-setup/network-tunnel-manager.sh | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 799ee4f6d2..fbba8cd5f2 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -4,11 +4,19 @@ set -euo pipefail +############################################################################### +# colors (no emojis) +############################################################################### +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[0;33m' +NC='\033[0m' + ############################################################################### # safety: must run as root, jq ############################################################################### if [ "$(id -u)" -ne 0 ]; then - echo "This script must be run as root" + echo -e "${RED}This script must be run as root${NC}" exit 1 fi @@ -16,6 +24,10 @@ fi # basic config ############################################################################### +NYM_CHAIN="NYM-EXIT" +POLICY_FILE="/etc/nym/exit-policy.txt" +EXIT_POLICY_LOCATION="https://nymtech.net/.wellknown/network-requester/exit-policy.txt" + TUNNEL_INTERFACE="${TUNNEL_INTERFACE:-nymtun0}" WG_INTERFACE="${WG_INTERFACE:-nymwg}" @@ -46,16 +58,6 @@ if [[ -z "$NETWORK_DEVICE" ]]; then exit 1 fi -NYM_CHAIN="NYM-EXIT" -POLICY_FILE="/etc/nym/exit-policy.txt" -EXIT_POLICY_LOCATION="https://nymtech.net/.wellknown/network-requester/exit-policy.txt" - -# colors (no emojis) -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[0;33m' -NC='\033[0m' - ############################################################################### # shared helpers ############################################################################### From 22db132c0909693fcf2071f17577accdeae0c61e Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Wed, 19 Nov 2025 14:14:19 +0100 Subject: [PATCH 107/180] @ merge fix test_default_reject_rule --- scripts/nym-node-setup/network-tunnel-manager.sh | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index fbba8cd5f2..6982e74baa 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -886,14 +886,14 @@ test_forward_chain_hook() { local failures=0 if iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then - echo echo -e "${GREEN}ipv4 forward hook ok: -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN${NC}" + echo -e "${GREEN}ipv4 forward hook ok: -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN${NC}" else echo -e "${RED}ipv4 forward hook missing or wrong${NC}" ((failures++)) fi if ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then - echo echo -e "${GREEN}ipv6 forward hook ok: -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN${NC}" + echo -e "${GREEN}ipv6 forward hook ok: -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN${NC}" else echo -e "${RED}ipv6 forward hook missing or wrong${NC}" ((failures++)) @@ -905,11 +905,6 @@ test_forward_chain_hook() { test_default_reject_rule() { echo -e "${YELLOW}testing default reject rule position in ${NYM_CHAIN}${NC}" - # not sure this will really check that it is on end - if iptables -L "$NYM_CHAIN" | grep -q "REJECT"; then - echo "default reject present in ipv4 chain" - else - echo "default reject missing in ipv4 chain" local last_rule_v4 last_rule_v4=$(iptables -S "$NYM_CHAIN" | awk '/^-A /{rule=$0} END{print rule}') if [[ "$last_rule_v4" != "-A $NYM_CHAIN -j REJECT --reject-with icmp-port-unreachable" ]]; then From 9c5847dc6764b9437128478c692333b2ce7a57bd Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Wed, 19 Nov 2025 14:26:16 +0100 Subject: [PATCH 108/180] @ fix failing exit_policy_run_tests --- scripts/nym-node-setup/network-tunnel-manager.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 6982e74baa..0cc57d5fb7 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -935,17 +935,17 @@ exit_policy_run_tests() { local failed=0 test_forward_chain_hook || ((failed += 1)) - ((total++)) + ((total += 1)) test_port_range_rules || ((failed += 1)) - ((total++)) + ((total += 1)) test_critical_services || ((failed += 1)) - ((total++)) + ((total += 1)) if [[ $skip_default -eq 0 ]]; then test_default_reject_rule || ((failed += 1)) - ((total++)) + ((total += 1)) fi echo "tests run: $total, failures: $failed" From 568268d39b6460e70c5b4518a8bc02fa45cab45a Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Wed, 19 Nov 2025 14:32:33 +0100 Subject: [PATCH 109/180] + color exit_policy_run_tests --- scripts/nym-node-setup/network-tunnel-manager.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 0cc57d5fb7..67f22af298 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -927,7 +927,7 @@ exit_policy_run_tests() { while [[ $# -gt 0 ]]; do case "$1" in --skip-default-reject) skip_default=1; shift ;; - *) echo "unknown test option: $1"; return 1 ;; + *) echo -e "${RED}unknown test option: $1${NC}"; return 1 ;; esac done @@ -948,11 +948,11 @@ exit_policy_run_tests() { ((total += 1)) fi - echo "tests run: $total, failures: $failed" + echo -e "${YELLOW}tests run: ${GREEN}$total${YELLOW}, failures: ${RED}$failed${NC}" if [[ $failed -eq 0 ]]; then - echo "all exit policy tests passed" + echo -e "${GREEN}all exit policy tests passed${NC}" else - echo "some exit policy tests failed" + echo -e "${RED}some exit policy tests failed${NC}" fi return "$failed" From 7a339d4c4d325b560c4d9f961f564d32c4396d0f Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Wed, 19 Nov 2025 16:35:40 +0100 Subject: [PATCH 110/180] + color everywhere --- .../nym-node-setup/network-tunnel-manager.sh | 86 +++++++++++-------- 1 file changed, 49 insertions(+), 37 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 67f22af298..c03ada3a08 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -12,11 +12,23 @@ RED='\033[0;31m' YELLOW='\033[0;33m' NC='\033[0m' +info() { + printf "%b\n" "${YELLOW}[INFO] $*${NC}" +} + +ok() { + printf "%b\n" "${GREEN}[OK] $*${NC}" +} + +error() { + printf "%b\n" "${RED}[ERROR] $*${NC}" +} + ############################################################################### # safety: must run as root, jq ############################################################################### if [ "$(id -u)" -ne 0 ]; then - echo -e "${RED}This script must be run as root${NC}" + error "This script must be run as root" exit 1 fi @@ -54,7 +66,7 @@ if [[ -z "$NETWORK_DEVICE" ]]; then NETWORK_DEVICE="$(detect_uplink_interface "ip -o route show default table all")" fi if [[ -z "$NETWORK_DEVICE" ]]; then - echo "cannot determine uplink interface. set NETWORK_DEVICE or UPLINK_DEV" + error "cannot determine uplink interface. set NETWORK_DEVICE or UPLINK_DEV" exit 1 fi @@ -63,19 +75,19 @@ fi ############################################################################### ensure_jq() { - echo "checking for jq..." + info "checking for jq..." if command -v jq >/dev/null 2>&1; then - echo "jq is already installed" + ok "jq is already installed" else - echo "jq not found, installing..." + info "jq not found, installing..." apt-get update -y DEBIAN_FRONTEND=noninteractive apt-get install -y jq if command -v jq >/dev/null 2>&1; then - echo "jq installed successfully" + ok "jq installed successfully" else - echo "failed to install jq" + error "failed to install jq" exit 1 fi fi @@ -810,7 +822,7 @@ test_exit_policy_connectivity() { ############################################################################### test_port_range_rules() { - echo "testing port range rules in ${NYM_CHAIN}" + info "testing port range rules in ${NYM_CHAIN}" local port_ranges=( "20-21:tcp:ftp" @@ -834,9 +846,9 @@ test_port_range_rules() { end=$(echo "$range" | cut -d'-' -f2) if iptables -t filter -C "$NYM_CHAIN" -p "$proto" --dport "$start:$end" -j ACCEPT 2>/dev/null; then - echo "rule ok: $name $proto $range" + ok "rule ok: $name $proto $range" else - echo "missing rule: $name $proto $range" + error "missing rule: $name $proto $range" ((failures++)) fi done @@ -845,7 +857,7 @@ test_port_range_rules() { } test_critical_services() { - echo "testing critical service rules in ${NYM_CHAIN}" + info "testing critical service rules in ${NYM_CHAIN}" local tcp_ports=(22 53 443 853 1194) local udp_ports=(53 123 1194) @@ -853,12 +865,12 @@ test_critical_services() { for port in "${tcp_ports[@]}"; do if iptables -t filter -C "$NYM_CHAIN" -p tcp --dport "$port" -j ACCEPT 2>/dev/null; then - echo "tcp port $port allowed" + ok "tcp port $port allowed" else if iptables-save | grep -E "^-A $NYM_CHAIN.*tcp.*dpts:" | grep -q "$port"; then - echo "tcp port $port allowed by range" + ok "tcp port $port allowed by range" else - echo "tcp port $port missing" + error "tcp port $port missing" ((failures++)) fi fi @@ -866,12 +878,12 @@ test_critical_services() { for port in "${udp_ports[@]}"; do if iptables -t filter -C "$NYM_CHAIN" -p udp --dport "$port" -j ACCEPT 2>/dev/null; then - echo "udp port $port allowed" + ok "udp port $port allowed" else if iptables-save | grep -E "^-A $NYM_CHAIN.*udp.*dpts:" | grep -q "$port"; then - echo "udp port $port allowed by range" + ok "udp port $port allowed by range" else - echo "udp port $port missing" + error "udp port $port missing" ((failures++)) fi fi @@ -881,21 +893,21 @@ test_critical_services() { } test_forward_chain_hook() { - echo -e "${YELLOW}testing forward chain hook direction for ${NYM_CHAIN}${NC}" + info "testing forward chain hook direction for ${NYM_CHAIN}" local failures=0 if iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then - echo -e "${GREEN}ipv4 forward hook ok: -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN${NC}" + ok "ipv4 forward hook ok: -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN" else - echo -e "${RED}ipv4 forward hook missing or wrong${NC}" + error "ipv4 forward hook missing or wrong" ((failures++)) fi if ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then - echo -e "${GREEN}ipv6 forward hook ok: -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN${NC}" + ok "ipv6 forward hook ok: -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN" else - echo -e "${RED}ipv6 forward hook missing or wrong${NC}" + error "ipv6 forward hook missing or wrong" ((failures++)) fi @@ -903,23 +915,23 @@ test_forward_chain_hook() { } test_default_reject_rule() { - echo -e "${YELLOW}testing default reject rule position in ${NYM_CHAIN}${NC}" + info "testing default reject rule position in ${NYM_CHAIN}" local last_rule_v4 last_rule_v4=$(iptables -S "$NYM_CHAIN" | awk '/^-A /{rule=$0} END{print rule}') if [[ "$last_rule_v4" != "-A $NYM_CHAIN -j REJECT --reject-with icmp-port-unreachable" ]]; then - echo -e "${RED}default reject missing or not last in ipv4 chain${NC}" + error "default reject missing or not last in ipv4 chain" return 1 fi local last_rule_v6 last_rule_v6=$(ip6tables -S "$NYM_CHAIN" | awk '/^-A /{rule=$0} END{print rule}') if [[ "$last_rule_v6" != "-A $NYM_CHAIN -j REJECT --reject-with icmp6-port-unreachable" ]]; then - echo -e "${RED}default reject missing or not last in ipv6 chain${NC}" + error "default reject missing or not last in ipv6 chain" return 1 fi - echo -e "${GREEN}default reject confirmed at end of ${NYM_CHAIN}${NC}" + ok "default reject confirmed at end of ${NYM_CHAIN}" } exit_policy_run_tests() { @@ -927,7 +939,7 @@ exit_policy_run_tests() { while [[ $# -gt 0 ]]; do case "$1" in --skip-default-reject) skip_default=1; shift ;; - *) echo -e "${RED}unknown test option: $1${NC}"; return 1 ;; + *) error "unknown test option: $1"; return 1 ;; esac done @@ -948,11 +960,11 @@ exit_policy_run_tests() { ((total += 1)) fi - echo -e "${YELLOW}tests run: ${GREEN}$total${YELLOW}, failures: ${RED}$failed${NC}" + info "tests run: $total, failures: $failed" if [[ $failed -eq 0 ]]; then - echo -e "${GREEN}all exit policy tests passed${NC}" + ok "all exit policy tests passed" else - echo -e "${RED}some exit policy tests failed${NC}" + error "some exit policy tests failed" fi return "$failed" @@ -964,7 +976,7 @@ exit_policy_run_tests() { full_tunnel_setup() { # this mirrors your previous chain of calls but inside one script - echo "running full tunnel setup for ${TUNNEL_INTERFACE} and ${WG_INTERFACE}" + info "running full tunnel setup for ${TUNNEL_INTERFACE} and ${WG_INTERFACE}" check_tunnel_iptables "$TUNNEL_INTERFACE" remove_duplicate_rules "$TUNNEL_INTERFACE" @@ -985,11 +997,11 @@ full_tunnel_setup() { joke_through_tunnel "$TUNNEL_INTERFACE" joke_through_tunnel "$WG_INTERFACE" - echo "full tunnel setup completed" + ok "full tunnel setup completed" } exit_policy_install() { - echo "installing nym wireguard exit policy for ${WG_INTERFACE} via ${NETWORK_DEVICE}" + info "installing nym wireguard exit policy for ${WG_INTERFACE} via ${NETWORK_DEVICE}" exit_policy_install_deps adjust_ip_forwarding create_nym_chain @@ -998,17 +1010,17 @@ exit_policy_install() { apply_spamhaus_blocklist add_default_reject_rule save_iptables_rules - echo "nym exit policy installed" + ok "nym exit policy installed" } complete_networking_configuration() { - echo -e "${YELLOW}starting complete networking configuration: tunnels + exit policy${NC}" + info "starting complete networking configuration: tunnels + exit policy" full_tunnel_setup exit_policy_install - exit_policy_run_tests || echo -e "${RED}exit policy tests reported problems, please review output${NC}" + exit_policy_run_tests || error "exit policy tests reported problems, please review output" - echo "complete networking configuration finished" + ok "complete networking configuration finished" } ############################################################################### From 1b9af19e2082df7fb96ab1a9b153750f0b0da5c1 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:14:44 +0100 Subject: [PATCH 111/180] update routing configuration steps and make components --- .../operators/snippets/routing-conf.mdx | 247 ++++++++++++++++ .../snippets/wg-exit-policy-conf.mdx | 0 .../snippets/wg-exit-policy-test.mdx | 0 .../nodes/nym-node/configuration.mdx | 264 +----------------- 4 files changed, 254 insertions(+), 257 deletions(-) create mode 100644 documentation/docs/components/operators/snippets/routing-conf.mdx create mode 100644 documentation/docs/components/operators/snippets/wg-exit-policy-conf.mdx create mode 100644 documentation/docs/components/operators/snippets/wg-exit-policy-test.mdx diff --git a/documentation/docs/components/operators/snippets/routing-conf.mdx b/documentation/docs/components/operators/snippets/routing-conf.mdx new file mode 100644 index 0000000000..acb73b084d --- /dev/null +++ b/documentation/docs/components/operators/snippets/routing-conf.mdx @@ -0,0 +1,247 @@ +import { Callout } from 'nextra/components'; +import { Tabs } from 'nextra/components'; +import { VarInfo } from 'components/variable-info.tsx'; +import { Steps } from 'nextra/components'; +import { AccordionTemplate } from 'components/accordion-template.tsx'; + + +We're working on Rust implementation agregating all routing settings into a binary, to solve all connectivity configuration requirements. In the meantime node operators need to use the [`network_tunnel_manager.sh`](https://github.com/nymtech/nym/blob/develop/scripts/network_tunnel_manager.sh) as a handy support to configure their servers. + + + +Networking configuration across different ISPs and various operation systems does not have a generic solution. If the provided configuration setup doesn't solve your problem check out [IPv6 troubleshooting](/operators/troubleshooting/vps-isp.mdx#ipv6-troubleshooting) page. Be aware that you may have to do more research, customised adjustments or contact your ISP to change settings for your VPS. + + +**Network Tunnel Manager ([`network-tunnel-manager.sh`](https://github.com/nymtech/nym/blob/develop/scripts/network_tunnel_manager.sh), NTM) is currently the one tool hadling the configuration of `nym-node` hosting server, according to the required design (node's [functionality](/operators/nodes/nym-node/setup#functionality-mode), WireGuard setup etc).** + +**NTM cand administrate these areas:** + +* IPv4 and IPv6 routing to the internet + +* The `nymtun0` interface (Mixnet / 5-hop): dynamically managed by the `exit-gateway` service. When the service is stopped, `nymtun0` disappears, and when started, `nymtun0` is recreated. + +* The `nymwg` interface (WG / 2-hop): used for creating a secure wireguard tunnel as part of the Nym Network configuration. + +* `iptables` rules specific to `nymwg` to ensure proper routing and forwarding through the wireguard tunnel. The `nymwg` interface needs to be correctly configured and active for the related commands to function properly. This includes applying or removing iptables rules and running connectivity tests through the `nymwg` tunnel. + +* WireGuard exit policy: Mixnet uses a common [exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt), to apply the same for WG, the operators need to set that one up on their server using `iptables` rules. + +* Testing and validating all above + +The script should be used in a context where `nym-node` is running to fully utilise its capabilities, particularly for fetching IPv6 addresses or applying network rules that depend on the `nymtun0` and `nymwg` interfaces and to establish a WireGuard tunnel. + +**Before starting with the following configuration, make sure you have the [latest `nym-node` binary](https://github.com/nymtech/nym/releases) installed and your [VPS setup](../preliminary-steps/vps-setup.mdx) finished properly!** + + +**Run the following steps as root!** + + +|**Choose configuration command according your setup** + +
+ New nym-node Full Configuration, + Existing nym-node Full Configuration + Step-by-step or Partial Configuration + ]} defaultIndex={0}> + +This design is meant for operators setting up a new node on a fresh machine and it will result with a complete server readiness for routing as Entry Gateway and Exit Gateway in both Mixnet and WireGuard mode. + + +###### 1. Download `network-tunnel-manager.sh`, make executable and run with `--help` command: + +```sh +curl -L https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh -o network-tunnel-manager.sh && \ +chmod +x network-tunnel-manager.sh && \ +./network-tunnel-manager.sh --help +``` + +###### 2. Make sure your `nym-node` service is up and running and bonded + +- **If you setting up a new node and not upgrading an existing one, keep it running and [bond](/operators/nodes/nym-node/bonding.mdx) your node now! Then come back here and follow the rest of the configuration.** + +###### 3. Execute complete network configuration: +```sh +./network-tunnel-manager.sh complete_networking_setup +``` + + + + +This is meant for operators configuring an existing and bonded node and it will result with a complete server readiness for routing as Entry Gateway and Exit Gateway in both Mixnet and WireGuard mode. + + +###### 1. Download `network-tunnel-manager.sh`, make executable and run with `--help` command: + +```sh +curl -L https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh -o network-tunnel-manager.sh && \ +chmod +x network-tunnel-manager.sh && \ +./network-tunnel-manager.sh --help +``` + +###### 2. Execute complete network configuration: +```sh +./network-tunnel-manager.sh complete_networking_setup +``` + + + + +This design is meant for operators who want to do their server configuration step by step or choose only some parts of the setup. + +###### 1. Download `network-tunnel-manager.sh`, make executable and run with `--help` command: + +```sh +curl -L https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh -o network-tunnel-manager.sh && \ +chmod +x network-tunnel-manager.sh && \ +./network-tunnel-manager.sh --help +``` + +###### 2. Make sure your `nym-node` service is up and running and bonded + +- **If you setting up a new node and not upgrading an existing one, keep it running and [bond](/operators/nodes/nym-node/bonding.mdx) your node now! Then come back here and follow the rest of the configuration.** + +###### 3. Choose steps according your need + +> You should be certain in your selection when configuring only various parts of the server. + +###### Setup IP tables rules + +- Delete IP tables rules for IPv4 and IPv6 and apply new ones: +```sh +./network-tunnel-manager.sh remove_duplicate_rules nymtun0 + +./network-tunnel-manager.sh apply_iptables_rules +``` + +- The process may prompt you if you want to save current IPv4 and IPv6 rules, choose yes. + +![](/images/operators/ip_table_prompt.png) + +- At this point you should see a `global ipv6` address. +```sh +./network-tunnel-manager.sh fetch_and_display_ipv6 +``` +
+}> +```sh +iptables-persistent is already installed. +Using IPv6 address: 2001:db8:a160::1/112 #the address will be different for you +operation fetch_ipv6_address_nym_tun completed successfully. +``` + + +###### Check Nymtun IP tables: + +```sh +./network-tunnel-manager.sh check_nymtun_iptables +``` + +- If there's no process running it wouldn't return anything. +- In case you see `nymtun0` but not active, this is probably because you are setting up a new (never bonded) node and not upgrading an existing one. + +
+}> +```sh +iptables-persistent is already installed. +network Device: eth0 +--------------------------------------- + +inspecting IPv4 firewall rules... +Chain FORWARD (policy DROP 0 packets, 0 bytes) + 0 0 ufw-reject-forward all -- * * 0.0.0.0/0 0.0.0.0/0 + 0 0 ACCEPT all -- nymtun0 eth0 0.0.0.0/0 0.0.0.0/0 + 0 0 ACCEPT all -- eth0 nymtun0 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED + 0 0 ACCEPT all -- nymtun0 eth0 0.0.0.0/0 0.0.0.0/0 + 0 0 ACCEPT all -- eth0 nymtun0 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED + 0 0 ACCEPT all -- nymtun0 eth0 0.0.0.0/0 0.0.0.0/0 + 0 0 ACCEPT all -- eth0 nymtun0 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED +--------------------------------------- + +inspecting IPv6 firewall rules... +Chain FORWARD (policy DROP 0 packets, 0 bytes) + 0 0 ufw6-reject-forward all * * ::/0 ::/0 + 0 0 ACCEPT all eth0 nymtun0 ::/0 ::/0 state RELATED,ESTABLISHED + 0 0 ACCEPT all nymtun0 eth0 ::/0 ::/0 + 0 0 ACCEPT all eth0 nymtun0 ::/0 ::/0 state RELATED,ESTABLISHED + 0 0 ACCEPT all nymtun0 eth0 ::/0 ::/0 + 0 0 ACCEPT all eth0 nymtun0 ::/0 ::/0 state RELATED,ESTABLISHED + 0 0 ACCEPT all nymtun0 eth0 ::/0 ::/0 +operation check_nymtun_iptables completed successfully. +``` + + +###### Remove old and apply new rules for wireguad routing + +```sh +../network-tunnel-manager.sh remove_duplicate_rules nymwg + +./network-tunnel-manager.sh apply_iptables_rules_wg +``` + +###### Apply rules to configure DNS routing and allow ICMP piung test for node probing (network testing) + +```sh +./network-tunnel-manager.sh configure_dns_and_icmp_wg +``` +###### Adjust and validate IP forwarding + +```sh +./network-tunnel-manager.sh adjust_ip_forwarding + +./network-tunnel-manager.sh check_ipv6_ipv4_forwarding +``` + +###### Check `nymtun0` interface and test routing configuration + +```sh +ip addr show nymtun0 +``` + +
+}> +```sh +# your addresses will be different +8: nymtun0: mtu 1420 qdisc fq_codel state UNKNOWN group default qlen 500 + link/none + inet 10.0.0.1/16 scope global nymtun0 + valid_lft forever preferred_lft forever + inet6 fc00::1/112 scope global + valid_lft forever preferred_lft forever + inet6 fe80::ad08:d167:5700:8c7c/64 scope link stable-privacy + valid_lft forever preferred_lft forever` +``` + + +- Validate your IPv6 and IPv4 networking by running a joke test via Mixnet: +```sh +./network-tunnel-manager.sh joke_through_the_mixnet +``` + +- Validate your tunneling by running a joke test via WG: +```sh +../network-tunnel-manager.sh joke_through_wg_tunnel +``` + +###### Enable wireguard + +Now you can run your node with the `--wireguard-enabled true` flag or add it to your [systemd service config](#systemd). Restart your `nym-node` or [systemd](#2-following-steps-for-nym-nodes-running-as-systemd-service) service (recommended): + +```sh +systemctl daemon-reload && service nym-node restart +``` +- Optionally, you can check if the node is running correctly by monitoring the service logs: + +```sh +journalctl -u nym-node.service -f -n 100 +``` + +
+
+
+ + +Note that the functionality the node runs in is decided by [arguments on the node itself / in node's `config.toml`](/operators/nodes/nym-node/setup#functionality-mode), this tool only prepares the server. + + +Make sure that you get the validation of all connectivity. If there are still any problems, please refer to [troubleshooting section](/operators/troubleshooting/vps-isp.mdx#incorrect-gateway-network-check). diff --git a/documentation/docs/components/operators/snippets/wg-exit-policy-conf.mdx b/documentation/docs/components/operators/snippets/wg-exit-policy-conf.mdx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/documentation/docs/components/operators/snippets/wg-exit-policy-test.mdx b/documentation/docs/components/operators/snippets/wg-exit-policy-test.mdx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx index dbe24c5072..f1ab9af05e 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx @@ -6,12 +6,12 @@ import { AccordionTemplate } from 'components/accordion-template.tsx'; import ExitPolicyInstallOutput from 'components/operators/snippets/wg-exit-policy-install-output.mdx'; import ExitPolicyStatusOutput from 'components/operators/snippets/wg-exit-policy-status-output.mdx'; import ExitPolicyTestOutput from 'components/operators/snippets/wg-exit-policy-test-output.mdx'; -import ExitPolicyTestServer from 'components/operators/snippets/wg-exit-policy-testing-from-server.mdx'; -import ExitPolicyTestOutside from 'components/operators/snippets/wg-exit-policy-testing-from-outside.mdx'; +import WGExitPolicyConf from 'components/operators/snippets/wg-exit-policy-conf.mdx'; +import WGExitPolicyTest from 'components/operators/snippets/wg-exit-policy-test.mdx'; +import RoutingConf from 'components/operators/snippets/routing-conf.mdx'; import QuicDeploymentSteps from 'components/operators/snippets/quic-bridge-deployment-script-setup.mdx'; - -export const ManagerIPOutput = () => ( +Export const ManagerIPOutput = () => (
Correct ./network_tunnel_manager.sh fetch_and_display_ipv6 output
@@ -275,265 +275,15 @@ Make sure to keep your IPv4 address enabled while setting up IPv6, as the majori ### Routing Configuration -While we're working on Rust implementation to have these settings as a part of the binary build, to solve these connectivity requirements in the meantime we wrote a script [`network_tunnel_manager.sh`](https://github.com/nymtech/nym/blob/develop/scripts/network_tunnel_manager.sh) to support operators to configure their servers and address all the connectivity requirements. - -Networking configuration across different ISPs and various operation systems does not have a generic solution. If the provided configuration setup doesn't solve your problem check out [IPv6 troubleshooting](../../troubleshooting/vps-isp.mdx#ipv6-troubleshooting) page. Be aware that you may have to do more research, customised adjustments or contact your ISP to change settings for your VPS. - -The `nymtun0` interface is dynamically managed by the `exit-gateway` service. When the service is stopped, `nymtun0` disappears, and when started, `nymtun0` is recreated. - -The `nymwg` interface is used for creating a secure wireguard tunnel as part of the Nym Network configuration. Similar to `nymtun0`, the script manages iptables rules specific to `nymwg` to ensure proper routing and forwarding through the wireguard tunnel. The `nymwg` interface needs to be correctly configured and active for the related commands to function properly. This includes applying or removing iptables rules and running connectivity tests through the `nymwg` tunnel. - -The script should be used in a context where `nym-node` is running to fully utilise its capabilities, particularly for fetching IPv6 addresses or applying network rules that depend on the `nymtun0` and `nymwg` interfaces and to establish a WireGuard tunnel. - -**Before starting with the following configuration, make sure you have the [latest `nym-node` binary](https://github.com/nymtech/nym/releases) installed and your [VPS setup](../preliminary-steps/vps-setup.mdx) finished properly!** - - - -###### 1. Download `network_tunnel_manager.sh`, make executable and run: - -```sh -curl -L https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/network_tunnel_manager.sh -o network_tunnel_manager.sh && \ -chmod +x network_tunnel_manager.sh && \ -./network_tunnel_manager.sh -``` - -###### 2. Make sure your `nym-node` service is up and running and bond - -- **If you setting up a new node and not upgrading an existing one, keep it running and [bond](bonding.mdx) your node now**. Then come back here and follow the rest of the configuration. - - -**Run the following steps as root or with `sudo` prefix!** - - - -###### 3. Setup IP tables rules - -- Delete IP tables rules for IPv4 and IPv6 and apply new ones: -```sh -./network_tunnel_manager.sh remove_duplicate_rules nymtun0 - -./network_tunnel_manager.sh apply_iptables_rules -``` - -- The process may prompt you if you want to save current IPv4 and IPv6 rules, choose yes. - -![](/images/operators/ip_table_prompt.png) - -- At this point you should see a `global ipv6` address. -```sh -./network_tunnel_manager.sh fetch_and_display_ipv6 -``` -
-}> -```sh -iptables-persistent is already installed. -Using IPv6 address: 2001:db8:a160::1/112 #the address will be different for you -operation fetch_ipv6_address_nym_tun completed successfully. -``` - - -###### 4. Check Nymtun IP tables: - -```sh -./network_tunnel_manager.sh check_nymtun_iptables -``` - -- If there's no process running it wouldn't return anything. -- In case you see `nymtun0` but not active, this is probably because you are setting up a new (never bonded) node and not upgrading an existing one. - -
-}> -```sh -iptables-persistent is already installed. -network Device: eth0 ---------------------------------------- - -inspecting IPv4 firewall rules... -Chain FORWARD (policy DROP 0 packets, 0 bytes) - 0 0 ufw-reject-forward all -- * * 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT all -- nymtun0 eth0 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT all -- eth0 nymtun0 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED - 0 0 ACCEPT all -- nymtun0 eth0 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT all -- eth0 nymtun0 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED - 0 0 ACCEPT all -- nymtun0 eth0 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT all -- eth0 nymtun0 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED ---------------------------------------- - -inspecting IPv6 firewall rules... -Chain FORWARD (policy DROP 0 packets, 0 bytes) - 0 0 ufw6-reject-forward all * * ::/0 ::/0 - 0 0 ACCEPT all eth0 nymtun0 ::/0 ::/0 state RELATED,ESTABLISHED - 0 0 ACCEPT all nymtun0 eth0 ::/0 ::/0 - 0 0 ACCEPT all eth0 nymtun0 ::/0 ::/0 state RELATED,ESTABLISHED - 0 0 ACCEPT all nymtun0 eth0 ::/0 ::/0 - 0 0 ACCEPT all eth0 nymtun0 ::/0 ::/0 state RELATED,ESTABLISHED - 0 0 ACCEPT all nymtun0 eth0 ::/0 ::/0 -operation check_nymtun_iptables completed successfully. -``` - - -###### 5. Remove old and apply new rules for wireguad routing - -```sh -./network_tunnel_manager.sh remove_duplicate_rules nymwg - -./network_tunnel_manager.sh apply_iptables_rules_wg -``` - -###### 6. Apply rules to configure DNS routing and allow ICMP piung test for node probing (network testing) - -```sh -./network_tunnel_manager.sh configure_dns_and_icmp_wg -``` -###### 7. Adjust and validate IP forwarding - -```sh -./network_tunnel_manager.sh adjust_ip_forwarding - -./network_tunnel_manager.sh check_ipv6_ipv4_forwarding -``` - -###### 8. Check `nymtun0` interface and test routing configuration - -```sh -ip addr show nymtun0 -``` - -
-}> -```sh -# your addresses will be different -8: nymtun0: mtu 1420 qdisc fq_codel state UNKNOWN group default qlen 500 - link/none - inet 10.0.0.1/16 scope global nymtun0 - valid_lft forever preferred_lft forever - inet6 fc00::1/112 scope global - valid_lft forever preferred_lft forever - inet6 fe80::ad08:d167:5700:8c7c/64 scope link stable-privacy - valid_lft forever preferred_lft forever` -``` - - -- Validate your IPv6 and IPv4 networking by running a joke test via Mixnet: -```sh -./network_tunnel_manager.sh joke_through_the_mixnet -``` - -- Validate your tunneling by running a joke test via WG: -```sh -./network_tunnel_manager.sh joke_through_wg_tunnel -``` - -- **Note:** WireGuard will return only IPv4 joke, not IPv6. WG IPv6 is under development. Running IPR joke through the mixnet with `./network_tunnel_manager.sh joke_through_the_mixnet` should work with both IPv4 and IPv6! - - -###### 9. Enable wireguard - -Now you can run your node with the `--wireguard-enabled true` flag or add it to your [systemd service config](#systemd). Restart your `nym-node` or [systemd](#2-following-steps-for-nym-nodes-running-as-systemd-service) service (recommended): - -```sh -systemctl daemon-reload && service nym-node restart -``` -- Optionally, you can check if the node is running correctly by monitoring the service logs: - -```sh -journalctl -u nym-node.service -f -n 100 -``` -
- -Make sure that you get the validation of all connectivity. If there are still any problems, please refer to [troubleshooting section](../../troubleshooting/vps-isp.mdx#incorrect-gateway-network-check). + ## Wireguard Exit Policy Configuration -Nym Node running as Exit Gateway has contains multiple modules, one of them is Nym Network Requester(NR), routing TCP traffic to the internet. To make sure that the node is not just an open proxy, NR checks agains [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt) following these conditions (in this exact order): - -1. Do we explicitly block those IP addresses regardless of ports? -2. Do we allow those specific ports regardless of IPs? -3. Do block EVERYTHING else! - -The exit policy is same for all NRs, the content is shaped by an offchain governance of Nym Node operators on our [forum](https://forum.nym.com/t/poll-a-new-nym-exit-policy-for-exit-gateways-and-the-nym-mixnet-is-inbound/464). - -There is a caveat though. NR is only routing TCP streams and therefore any other type of routing is *not* filtered thorugh the exit policy. To ensure that Nym Nodes follow the same exit policy when routing IP packets through wireguard and don't act as open proxies, the operators have to set up these rules via IP tables rules. - -**Follow these steps, using a [setup script](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh) and [testing scripts](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/exit-policy-tests.sh) written by Nym quality assurance team, to setup exit policy for wireguard:** - - - -###### 1. Download the scripts and make executable - -- SSH to your node -- Create a folder `~/nym-binaries` and navigate there -```sh -mkdir $HOME/nym-binaries -cd $HOME/nym-binaries -``` -- Download the scripts -```sh -wget https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh - -wget https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/exit-policy-tests.sh -``` -- Make executable -```sh -chmod +x wireguard-exit-policy-manager.sh exit-policy-tests.sh -``` - -###### 2. Install `wireguard-exit-policy-manager.sh` -```sh -./wireguard-exit-policy-manager.sh install -``` -- The output should look like this: - - - - - -###### 3. Run `wireguard-exit-policy-manager.sh` -```sh -./wireguard-exit-policy-manager.sh status -``` - -- The output should look like this: - - - - -###### 4. Test with `exit-policy-tests.sh` - -```sh -./exit-policy-tests.sh -``` - -- The output should look like this: - - - - -###### 5. In case of problems, you can clear the exit policy rule -```sh -./wireguard-exit-policy-manager.sh clear - -./wireguard-exit-policy-manager.sh status -``` - - -Now your wireguart routing should have same rotuing permissions like [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt) used on 5-hop (Mixnet) mode of NymVPN. + ### Testing Wireguard Exit Policy -You can validate the application of the IP tables routes on your `nym-node` by checking it from the server side as well as from the outside. - -
- From the server, - From the outside - using NymVPN - ]} defaultIndex={0}> - - - -
- -Your node has successfully implemented wireguard exit policy with the same routing permissions like [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt) used on 5-hop (Mixnet). + ## QUIC Transport Bridge Deployment From 45a1074377edd7f93af1c57ce76e7135cb71d051 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:35:33 +0100 Subject: [PATCH 112/180] remove redundant --- .../operators/snippets/routing-conf.mdx | 30 ++++++++++++++----- .../nyx-outputs/nyx-percent-stake.md | 2 +- .../nyx-outputs/nyx-total-stake.md | 2 +- .../outputs/api-scraping-outputs/time-now.md | 2 +- .../nodes/nym-node/configuration.mdx | 16 ---------- 5 files changed, 25 insertions(+), 27 deletions(-) diff --git a/documentation/docs/components/operators/snippets/routing-conf.mdx b/documentation/docs/components/operators/snippets/routing-conf.mdx index acb73b084d..9910043512 100644 --- a/documentation/docs/components/operators/snippets/routing-conf.mdx +++ b/documentation/docs/components/operators/snippets/routing-conf.mdx @@ -1,9 +1,26 @@ import { Callout } from 'nextra/components'; import { Tabs } from 'nextra/components'; -import { VarInfo } from 'components/variable-info.tsx'; import { Steps } from 'nextra/components'; import { AccordionTemplate } from 'components/accordion-template.tsx'; +export const ManagerIPOutput = () => ( +
+ Correct ./network-tunnel-manager.sh fetch_and_display_ipv6 output +
+); + +export const ManagerTablesOutput = () => ( +
+ Correct ./network-tunnel-manager.sh check_nymtun_iptables output +
+); + +export const ShowTun = () => ( +
+ Correct ip addr show nymtun0 output +
+); + We're working on Rust implementation agregating all routing settings into a binary, to solve all connectivity configuration requirements. In the meantime node operators need to use the [`network_tunnel_manager.sh`](https://github.com/nymtech/nym/blob/develop/scripts/network_tunnel_manager.sh) as a handy support to configure their servers. @@ -28,20 +45,18 @@ Networking configuration across different ISPs and various operation systems doe * Testing and validating all above -The script should be used in a context where `nym-node` is running to fully utilise its capabilities, particularly for fetching IPv6 addresses or applying network rules that depend on the `nymtun0` and `nymwg` interfaces and to establish a WireGuard tunnel. - **Before starting with the following configuration, make sure you have the [latest `nym-node` binary](https://github.com/nymtech/nym/releases) installed and your [VPS setup](../preliminary-steps/vps-setup.mdx) finished properly!** **Run the following steps as root!** - + -|**Choose configuration command according your setup** +**Choose configuration command according your setup**
New nym-node Full Configuration, - Existing nym-node Full Configuration + New nym-node Full Configuration, + Existing nym-node Full Configuration, Step-by-step or Partial Configuration ]} defaultIndex={0}> @@ -67,7 +82,6 @@ chmod +x network-tunnel-manager.sh && \ - This is meant for operators configuring an existing and bonded node and it will result with a complete server readiness for routing as Entry Gateway and Exit Gateway in both Mixnet and WireGuard mode. diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md index 069e6eb141..858227b92a 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md @@ -1 +1 @@ -0.77% +0.79% diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md index 486924e832..6c69256b94 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md @@ -1 +1 @@ -37.305 +36.446 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md index 3351fa154b..8e492903cb 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md @@ -1 +1 @@ -Monday, November 10th 2025, 13:30:27 UTC +Wednesday, November 19th 2025, 16:17:05 UTC diff --git a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx index f1ab9af05e..e8a824d287 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx @@ -11,23 +11,7 @@ import WGExitPolicyTest from 'components/operators/snippets/wg-exit-policy-test. import RoutingConf from 'components/operators/snippets/routing-conf.mdx'; import QuicDeploymentSteps from 'components/operators/snippets/quic-bridge-deployment-script-setup.mdx'; -Export const ManagerIPOutput = () => ( -
- Correct ./network_tunnel_manager.sh fetch_and_display_ipv6 output -
-); -export const ManagerTablesOutput = () => ( -
- Correct ./network_tunnel_manager.sh check_nymtun_iptables output -
-); - -export const ShowTun = () => ( -
- Correct ip addr show nymtun0 output -
-); From 37e3a101b177de071be05a0506084b3fd406d96c Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:41:35 +0100 Subject: [PATCH 113/180] fix routing test --- .../operators/snippets/routing-conf.mdx | 9 +++++++ .../nodes/nym-node/configuration.mdx | 25 +++++-------------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/documentation/docs/components/operators/snippets/routing-conf.mdx b/documentation/docs/components/operators/snippets/routing-conf.mdx index 9910043512..12549970f6 100644 --- a/documentation/docs/components/operators/snippets/routing-conf.mdx +++ b/documentation/docs/components/operators/snippets/routing-conf.mdx @@ -21,6 +21,15 @@ export const ShowTun = () => (
); + + +We recommend operators to configure their `nym-node` with the full routing configuration. + +However, most of the time the packets sent through the Mixnet are IPv4 based. The IPv6 packets are still pretty rare and therefore it's not mandatory from operational point of view to have this configuration implemented if you running only `mixnode` mode. + +If you preparing to run a `nym-node` with all modes enabled in the future, this setup is required. + + We're working on Rust implementation agregating all routing settings into a binary, to solve all connectivity configuration requirements. In the meantime node operators need to use the [`network_tunnel_manager.sh`](https://github.com/nymtech/nym/blob/develop/scripts/network_tunnel_manager.sh) as a handy support to configure their servers. diff --git a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx index e8a824d287..af8bfdf590 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx @@ -206,9 +206,13 @@ This lets your operating system know it's ok to reload the service configuration
-## Connectivity Test and Configuration +## Routing Configuration -During our ongoing testing events we found out, that after introducing IP Packet Router (IPR) and [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt) on embedded Network Requester (NR) by default, only a fragment of Gateways routes correctly through IPv4 and IPv6. We built a useful monitor to check out your Gateway (`nym-node --mode exit-gateway`) at [harbourmaster.nymtech.net](https://harbourmaster.nymtech.net/). + + +## Routing Validation & Test + +### Quick IPv6 Check IPv6 routing is not only a case for gateways. Imagine a rare occasion when you run a `mixnode` without IPv6 enabled and a client will sent IPv6 packets through the Mixnet through such route: ```ascii @@ -216,19 +220,6 @@ IPv6 routing is not only a case for gateways. Imagine a rare occasion when you r ``` In this (unusual) case your `mixnode` will not be able to route the packets. The node will drop the packets and its performance would go down. For that reason it's beneficial to have IPv6 enabled when running a `mixnode` functionality. - -We recommend operators to configure their `nym-node` with the full routing configuration. - -However, most of the time the packets sent through the Mixnet are IPv4 based. The IPv6 packets are still pretty rare and therefore it's not mandatory from operational point of view to have this configuration implemented if you running only `mixnode` mode. - -If you preparing to run a `nym-node` with all modes enabled in the future, this setup is required. - - - -For everyone participating in Delegation Program or Service Grant program, this setup is a requirement! - - -### Quick IPv6 Check You can always check IPv6 address and connectivity by using some of these methods:
@@ -257,10 +248,6 @@ telnet -6 ipv6.telnetmyip.com Make sure to keep your IPv4 address enabled while setting up IPv6, as the majority of routing goes through that one!
-### Routing Configuration - - - ## Wireguard Exit Policy Configuration From b8479e1cdea774d498180824bb8e7a737f29ba6b Mon Sep 17 00:00:00 2001 From: PaJaSoft <101021895+PaJaSoft@users.noreply.github.com> Date: Thu, 20 Nov 2025 01:24:16 +0100 Subject: [PATCH 114/180] Update chain registry link --- documentation/docs/pages/operators/troubleshooting/nodes.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/pages/operators/troubleshooting/nodes.mdx b/documentation/docs/pages/operators/troubleshooting/nodes.mdx index 64c09e4a6c..0011a9a6b4 100644 --- a/documentation/docs/pages/operators/troubleshooting/nodes.mdx +++ b/documentation/docs/pages/operators/troubleshooting/nodes.mdx @@ -113,7 +113,7 @@ There are a few community explorers as well. Enter your **identity key** to find your node. Check the contents of the `Mixnode stats` and `Routing score` sections. -[Here](https://github.com/cosmos/chain-registry/blob/master/nyx/chain.json#L158-L187) is a dictionary with Nyx chain registry entry regarding all explorers. +[Here](https://github.com/cosmos/chain-registry/blob/master/nyx/chain.json#L183-L225) is a dictionary with Nyx chain registry entry regarding all explorers. If you want more information, or if your node isn't showing up on the explorer of your choice and you want to double-check, here are some examples on how to check if the node is configured properly. From 90e07d99804d5746f22162810c65c9e22e01ed14 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Fri, 24 Oct 2025 12:08:16 +0200 Subject: [PATCH 115/180] weighted scoring and unit test --- .../nym-node-status-api/src/http/models.rs | 146 ++++++++++++++++-- 1 file changed, 136 insertions(+), 10 deletions(-) diff --git a/nym-node-status-api/nym-node-status-api/src/http/models.rs b/nym-node-status-api/nym-node-status-api/src/http/models.rs index 2721115068..3d4b9fbf68 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/models.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/models.rs @@ -387,13 +387,14 @@ fn calculate_mixnet_score(gateway: &Gateway) -> ScoreValue { } } -/// calculates a visual score for the gateway +/// calculates a visual score for the gateway using weighted metrics fn calculate_score(gateway: &Gateway, probe_outcome: &LastProbeResult) -> ScoreValue { let mixnet_performance = gateway.performance as f64 / 100.0; - let ping_ips_performance = probe_outcome + + let (download_speed_score, ping_ips_performance) = probe_outcome .outcome .wg - .clone() + .as_ref() .map(|p| { let ping_ips_performance = p.ping_ips_performance_v4 as f64; @@ -419,18 +420,19 @@ fn calculate_score(gateway: &Gateway, probe_outcome: &LastProbeResult) -> ScoreV 0.1 }; - // combine the scores - file_download_score * ping_ips_performance + (file_download_score, ping_ips_performance) }) - .unwrap_or(0f64); + .unwrap_or((0.0, 0.0)); - let score = mixnet_performance * ping_ips_performance; + // Weighted scoring: mixnet performance (40%), download speed (30%), ping performance (30%) + let weighted_score = + (mixnet_performance * 0.4) + (download_speed_score * 0.3) + (ping_ips_performance * 0.3); - if score > 0.75 { + if weighted_score > 0.75 { ScoreValue::High - } else if score > 0.5 { + } else if weighted_score > 0.5 { ScoreValue::Medium - } else if score > 0.1 { + } else if weighted_score > 0.1 { ScoreValue::Low } else { ScoreValue::Offline @@ -733,6 +735,130 @@ mod test { assert!(service.mixnet_websockets.is_none()); assert!(service.last_successful_ping_utc.is_none()); } + + #[test] + fn test_weighted_score_calculation() { + use crate::http::models::directory_gw_probe_outcome::EntryTestResult; + use crate::http::models::wg_outcome_versions::ProbeOutcomeV1; + + // Helper function to create a test gateway + fn create_test_gateway(performance: u8) -> Gateway { + Gateway { + gateway_identity_key: "test_key".to_string(), + bonded: true, + performance, + self_described: None, + explorer_pretty_bond: None, + description: nym_node_requests::api::v1::node::models::NodeDescription { + moniker: "test".to_string(), + details: "test".to_string(), + security_contact: "test@example.com".to_string(), + website: "https://example.com".to_string(), + }, + last_probe_result: None, + last_probe_log: None, + last_testrun_utc: None, + last_updated_utc: "2025-10-10T00:00:00Z".to_string(), + routing_score: 0.0, + config_score: 0, + bridges: None, + } + } + + // Helper function to create a test probe outcome + fn create_test_probe_outcome( + download_speed_mbps: f64, + ping_ips_performance: f32, + ) -> LastProbeResult { + let duration_sec = 1.0; + let file_size_mb = download_speed_mbps; + + LastProbeResult { + node: "test_node".to_string(), + used_entry: "test_entry".to_string(), + outcome: ProbeOutcome { + as_entry: directory_gw_probe_outcome::Entry::Tested(EntryTestResult { + can_connect: true, + can_route: true, + }), + as_exit: None, + wg: Some(ProbeOutcomeV1 { + can_register: true, + can_handshake: Some(true), + can_resolve_dns: Some(true), + ping_hosts_performance: Some(ping_ips_performance), + ping_ips_performance: Some(ping_ips_performance), + can_query_metadata_v4: Some(true), + can_handshake_v4: true, + can_resolve_dns_v4: true, + ping_hosts_performance_v4: ping_ips_performance, + ping_ips_performance_v4: ping_ips_performance, + can_handshake_v6: true, + can_resolve_dns_v6: true, + ping_hosts_performance_v6: ping_ips_performance, + ping_ips_performance_v6: ping_ips_performance, + download_duration_sec_v4: (duration_sec * 1000.0) as u64, + download_duration_milliseconds_v4: Some((duration_sec * 1000.0) as u64), + downloaded_file_size_bytes_v4: Some( + (file_size_mb * 1024.0 * 1024.0) as u64, + ), + downloaded_file_v4: "test".to_string(), + download_error_v4: "".to_string(), + download_duration_sec_v6: 0, + download_duration_milliseconds_v6: Some(0), + downloaded_file_size_bytes_v6: Some(0), + downloaded_file_v6: "".to_string(), + download_error_v6: "".to_string(), + }), + }, + } + } + + // Test case 1: Excellent node (should be High) + let gateway = create_test_gateway(90); // 90% mixnet performance + let probe = create_test_probe_outcome(6.0, 1.0); // 6 Mbps, 100% ping + let score = calculate_score(&gateway, &probe); + assert_eq!(score, ScoreValue::High, "Excellent node should be High"); + + // Test case 2: Good node (should be High with weighted scoring) + let gateway = create_test_gateway(90); // 90% mixnet performance + let probe = create_test_probe_outcome(3.0, 0.9); // 3 Mbps (0.75 score), 90% ping + let score = calculate_score(&gateway, &probe); + assert_eq!( + score, + ScoreValue::High, + "Good node should be High with weighted scoring" + ); + + // Test case 3: Medium node + let gateway = create_test_gateway(80); // 80% mixnet performance + let probe = create_test_probe_outcome(1.5, 0.8); // 1.5 Mbps (0.5 score), 80% ping + let score = calculate_score(&gateway, &probe); + assert_eq!(score, ScoreValue::Medium, "Medium node should be Medium"); + + // Test case 4: Poor node + let gateway = create_test_gateway(60); // 60% mixnet performance + let probe = create_test_probe_outcome(0.3, 0.3); // 0.3 Mbps (0.1 score), 30% ping + let score = calculate_score(&gateway, &probe); + assert_eq!(score, ScoreValue::Low, "Poor node should be Low"); + + // Test case 5: Failed node + let gateway = create_test_gateway(10); // 10% mixnet performance + let probe = create_test_probe_outcome(0.1, 0.0); // 0.1 Mbps (0.1 score), 0% ping + let score = calculate_score(&gateway, &probe); + assert_eq!(score, ScoreValue::Offline, "Failed node should be Offline"); + + // Test case 6: Edge case - just above threshold + let gateway = create_test_gateway(76); // 76% mixnet performance + let probe = create_test_probe_outcome(2.1, 0.75); // 2.1 Mbps (0.75 score), 75% ping + let score = calculate_score(&gateway, &probe); + // Weighted: (0.76 * 0.4) + (0.75 * 0.3) + (0.75 * 0.3) = 0.304 + 0.225 + 0.225 = 0.754 + assert_eq!( + score, + ScoreValue::High, + "Edge case just above 0.75 threshold should be High" + ); + } } #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] From b4544c2b48916d8be44d401ee05e2644abd7bafc Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:17:40 +0100 Subject: [PATCH 116/180] wg exit policy setup --- .../snippets/wg-exit-policy-conf.mdx | 69 +++++++++++++++++++ .../nodes/nym-node/configuration.mdx | 7 -- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/documentation/docs/components/operators/snippets/wg-exit-policy-conf.mdx b/documentation/docs/components/operators/snippets/wg-exit-policy-conf.mdx index e69de29bb2..1c99f69e6d 100644 --- a/documentation/docs/components/operators/snippets/wg-exit-policy-conf.mdx +++ b/documentation/docs/components/operators/snippets/wg-exit-policy-conf.mdx @@ -0,0 +1,69 @@ +import { Callout } from 'nextra/components'; +import { Tabs } from 'nextra/components'; +import { Steps } from 'nextra/components'; +import { AccordionTemplate } from 'components/accordion-template.tsx'; +import ExitPolicyInstallOutput from 'components/operators/snippets/wg-exit-policy-install-output.mdx'; +import ExitPolicyStatusOutput from 'components/operators/snippets/wg-exit-policy-status-output.mdx'; + + +**In case you had used `network-tunnel-manager.sh` with the command `complete_networking_setup`, your WireGuard exit policy is already setup. You can test it in the next chapter.** + + +Nym Node running as Exit Gateway has contains multiple modules, one of them is Nym Network Requester(NR), routing TCP traffic to the internet. To make sure that the node is not just an open proxy, NR checks agains [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt) following these conditions (in this exact order): + +1. Do we explicitly block those IP addresses regardless of ports? +2. Do we allow those specific ports regardless of IPs? +3. Do block EVERYTHING else! + +The exit policy is same for all NRs, the content is shaped by an offchain governance of Nym Node operators on our [forum](https://forum.nym.com/t/poll-a-new-nym-exit-policy-for-exit-gateways-and-the-nym-mixnet-is-inbound/464). + +There is a caveat though. NR is only routing TCP streams and therefore any other type of routing than Mixnet is *not* filtered thorugh the exit policy. To ensure that Nym Nodes follow the same exit policy when routing IP packets through WireGuard and don't act as open proxies, the operators have to set up these rules via IP tables rules. + +**For all routing configuration we provide one tool [`network-tunnel-manager.sh` (NTM)](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh). This tool manages WireGuard exit policy as well.** + +In case you haven't run `network-tunnel-manager.sh` with the command `complete_networking_setup` you need to use NTM for WireGuard exit policy configuration. + +**Folow these steps** + + +**Run the following steps as root!** + + + + +###### 1. Download `network-tunnel-manager.sh`, make executable and run with `--help` command: + +```sh +curl -L https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh -o network-tunnel-manager.sh && \ +chmod +x network-tunnel-manager.sh && \ +./network-tunnel-manager.sh --help +``` + +###### 2. Install exit policy + +- Clear old rules and configure new ones: +```sh +./network-tunnel-manager.sh exit_policy_clear +./network-tunnel-manager.sh exit_policy_install +``` +- The output should look like this: + + + + + +###### 3. Check status of your configuration +```sh +./network-tunnel-manager.sh exit_policy_status +``` + +- The output should look like this: + + + +``` + + +Now your WireGuard routing (2-hop) should have same rotuing permissions like [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt) used on 5-hop (Mixnet) mode of NymVPN. + + diff --git a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx index af8bfdf590..bd5e1319b3 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx @@ -3,18 +3,11 @@ import { Tabs } from 'nextra/components'; import { VarInfo } from 'components/variable-info.tsx'; import { Steps } from 'nextra/components'; import { AccordionTemplate } from 'components/accordion-template.tsx'; -import ExitPolicyInstallOutput from 'components/operators/snippets/wg-exit-policy-install-output.mdx'; -import ExitPolicyStatusOutput from 'components/operators/snippets/wg-exit-policy-status-output.mdx'; -import ExitPolicyTestOutput from 'components/operators/snippets/wg-exit-policy-test-output.mdx'; import WGExitPolicyConf from 'components/operators/snippets/wg-exit-policy-conf.mdx'; import WGExitPolicyTest from 'components/operators/snippets/wg-exit-policy-test.mdx'; import RoutingConf from 'components/operators/snippets/routing-conf.mdx'; import QuicDeploymentSteps from 'components/operators/snippets/quic-bridge-deployment-script-setup.mdx'; - - - - # Nym Node Configuration From 78fb7790102ca485ab22a4dd0b81d146c505db7a Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:33:06 +0100 Subject: [PATCH 117/180] write wg exit policy testing steps --- .../snippets/wg-exit-policy-test.mdx | 39 +++++++++++++++++++ .../wg-exit-policy-testing-from-outside.mdx | 24 +++++++++++- .../wg-exit-policy-testing-from-server.mdx | 34 ++++++++++++++-- 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/documentation/docs/components/operators/snippets/wg-exit-policy-test.mdx b/documentation/docs/components/operators/snippets/wg-exit-policy-test.mdx index e69de29bb2..f8a25006f3 100644 --- a/documentation/docs/components/operators/snippets/wg-exit-policy-test.mdx +++ b/documentation/docs/components/operators/snippets/wg-exit-policy-test.mdx @@ -0,0 +1,39 @@ +import { Tabs } from 'nextra/components'; +import { Steps } from 'nextra/components'; + +import ExitPolicyTestServer from 'components/operators/snippets/wg-exit-policy-testing-from-server.mdx'; +import ExitPolicyTestOutside from 'components/operators/snippets/wg-exit-policy-testing-from-outside.mdx'; + +**For all routing configuration we provide one tool [`network-tunnel-manager.sh` (NTM)](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh). This tool manages WireGuard tests all configurations, including WireGuard exit policy as well.** + + +You can use NTM to validate the application of the IP tables routes on your `nym-node` by checking it from the server side as well as from the outside. + +
+ From the server, + From the outside - using NymVPN + ]} defaultIndex={0}> + + + +
+ + +If all works , your node has successfully implemented WireGuard exit policy with the same routing permissions like [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt) used on 5-hop (Mixnet) for TCP routing. + + + + + + + + + + + + + + + + diff --git a/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-outside.mdx b/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-outside.mdx index ebc11c315c..35b61745db 100644 --- a/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-outside.mdx +++ b/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-outside.mdx @@ -1,9 +1,29 @@ +import { Steps } from 'nextra/components'; + +Here are a few ways you can ensre your WireGuard exit policy is working correctly from the outside. + + + +###### 1. Using NymVPN + +- Connect to NymVPN and use your node as an Exit Gateway in dVPN (2-hop) mode + +- While connected to NymVPN, navigate to [`portquiz.net:12345`](http://portquiz.net:12345) and see if you can load the page + +- It shouldn't load, but if you navigate to some of the accepted ports, like[`portquiz.net:443`](http://portquiz.net:443) it should all work + +###### 2. Test it from your local terminal + - Install these dependencies on your local machine: ```shell sudo apt install tcpdump sudo tcpdump -i nymwg -n ``` -- Connect to [NymVPN](https://nym.com) and select your node as an Exit Gateway (after running the exit policy [manager script](#wireguard-exit-policy-configuration)) +- Connect to [NymVPN](https://nym.com) and select your node as an Exit Gateway + - Run the `tcpdump` command before registering -- Have the output of the `echo $BLOCKED_IP` from your node and try to go into your browser or a registered client and try to connect - It will not resolve. + +- Have the output of the `echo $BLOCKED_IP` from your node and try to go into your browser or a registered client and try to connect - It should not resolve + + \ No newline at end of file diff --git a/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-server.mdx b/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-server.mdx index 02fe54adfa..501dcc3876 100644 --- a/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-server.mdx +++ b/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-server.mdx @@ -1,11 +1,39 @@ -Run this command to define variable `BLOCKED_IP` and try to `ping` it: +import { Steps } from 'nextra/components'; +import { AccordionTemplate } from 'components/accordion-template.tsx'; +import ExitPolicyTestOutput from 'components/operators/snippets/wg-exit-policy-test-output.mdx'; + + + + +###### 1. Make sure to have the latest NTM: +```sh +curl -L https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh -o network-tunnel-manager.sh && \ +chmod +x network-tunnel-manager.sh && \ +./network-tunnel-manager.sh --help +``` + +###### 2. Run tests with NTM +```sh +./network-tunnel-manager.sh exit_policy_test_connectivity +./network-tunnel-manager.sh exit_policy_tests +``` + +- The output should look like this: + + + + +###### 3. Optionally you can do some manual sanity checks +- Run this command to define variable `BLOCKED_IP` and try to `ping` it: ```shell BLOCKED_IP=$(grep "ExitPolicy reject" /etc/nym/exit-policy.txt | head -1 | sed -E 's/ExitPolicy reject ([^:]+):.*/\1/' | sed 's/\/.*$//') ping -c 3 $BLOCKED_IP ``` -You should see `100% packet loss` as an outcome. +- You should see `100% packet loss` as an outcome. ```shell telnet $BLOCKED_IP 80 ``` -You should see `telnet: Unable to connect to remote host: Connection timed out`. +- You should see `telnet: Unable to connect to remote host: Connection timed out`. + + \ No newline at end of file From dcfd0f77ad39b353e7dfe6a00f91775e7e65ebd4 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:39:11 +0100 Subject: [PATCH 118/180] debug trace ticks --- .../docs/components/operators/snippets/wg-exit-policy-conf.mdx | 1 - .../docs/components/outputs/api-scraping-outputs/time-now.md | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/documentation/docs/components/operators/snippets/wg-exit-policy-conf.mdx b/documentation/docs/components/operators/snippets/wg-exit-policy-conf.mdx index 1c99f69e6d..7d5ebf3de7 100644 --- a/documentation/docs/components/operators/snippets/wg-exit-policy-conf.mdx +++ b/documentation/docs/components/operators/snippets/wg-exit-policy-conf.mdx @@ -61,7 +61,6 @@ chmod +x network-tunnel-manager.sh && \ -``` Now your WireGuard routing (2-hop) should have same rotuing permissions like [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt) used on 5-hop (Mixnet) mode of NymVPN. diff --git a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md index 8e492903cb..bfd8731655 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md @@ -1 +1 @@ -Wednesday, November 19th 2025, 16:17:05 UTC +Thursday, November 20th 2025, 12:33:19 UTC From 47c6006bb7ac4285bdf262076585e14c367807c0 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:52:41 +0100 Subject: [PATCH 119/180] ready to merge back --- .../components/operators/snippets/routing-conf.mdx | 10 +++------- .../snippets/wg-exit-policy-testing-from-outside.mdx | 2 +- .../pages/operators/nodes/nym-node/configuration.mdx | 2 -- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/documentation/docs/components/operators/snippets/routing-conf.mdx b/documentation/docs/components/operators/snippets/routing-conf.mdx index 12549970f6..ebb33e5ba9 100644 --- a/documentation/docs/components/operators/snippets/routing-conf.mdx +++ b/documentation/docs/components/operators/snippets/routing-conf.mdx @@ -30,10 +30,6 @@ However, most of the time the packets sent through the Mixnet are IPv4 based. Th If you preparing to run a `nym-node` with all modes enabled in the future, this setup is required.
- -We're working on Rust implementation agregating all routing settings into a binary, to solve all connectivity configuration requirements. In the meantime node operators need to use the [`network_tunnel_manager.sh`](https://github.com/nymtech/nym/blob/develop/scripts/network_tunnel_manager.sh) as a handy support to configure their servers. - - Networking configuration across different ISPs and various operation systems does not have a generic solution. If the provided configuration setup doesn't solve your problem check out [IPv6 troubleshooting](/operators/troubleshooting/vps-isp.mdx#ipv6-troubleshooting) page. Be aware that you may have to do more research, customised adjustments or contact your ISP to change settings for your VPS. @@ -64,9 +60,9 @@ Networking configuration across different ISPs and various operation systems doe
New nym-node Full Configuration, - Existing nym-node Full Configuration, - Step-by-step or Partial Configuration + New nym-node full configuration, + Existing nym-node full configuration, + Step-by-step or Pprtial configuration ]} defaultIndex={0}> This design is meant for operators setting up a new node on a fresh machine and it will result with a complete server readiness for routing as Entry Gateway and Exit Gateway in both Mixnet and WireGuard mode. diff --git a/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-outside.mdx b/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-outside.mdx index 35b61745db..349a7cd2de 100644 --- a/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-outside.mdx +++ b/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-outside.mdx @@ -12,7 +12,7 @@ Here are a few ways you can ensre your WireGuard exit policy is working correctl - It shouldn't load, but if you navigate to some of the accepted ports, like[`portquiz.net:443`](http://portquiz.net:443) it should all work -###### 2. Test it from your local terminal +###### 2. Testing from your local terminal - Install these dependencies on your local machine: ```shell diff --git a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx index bd5e1319b3..d791e7f946 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx @@ -203,8 +203,6 @@ This lets your operating system know it's ok to reload the service configuration -## Routing Validation & Test - ### Quick IPv6 Check IPv6 routing is not only a case for gateways. Imagine a rare occasion when you run a `mixnode` without IPv6 enabled and a client will sent IPv6 packets through the Mixnet through such route: From 4e7b4715b08f0afd1bf37986ec61d6d380a47a38 Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Thu, 20 Nov 2025 14:37:46 +0100 Subject: [PATCH 120/180] / test failed echo text --- scripts/nym-node-setup/network-tunnel-manager.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index c03ada3a08..66db28a570 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -960,7 +960,7 @@ exit_policy_run_tests() { ((total += 1)) fi - info "tests run: $total, failures: $failed" + echo "tests run: $total, test failed: $failed" if [[ $failed -eq 0 ]]; then ok "all exit policy tests passed" else From 752c7915b3acccbaf139dd9cc8e53688544422cf Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Thu, 20 Nov 2025 14:47:41 +0100 Subject: [PATCH 121/180] + colors for check the firewall setup --- .../nym-node-setup/network-tunnel-manager.sh | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index ee1e3a7555..b4e6fdfb35 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -829,16 +829,16 @@ check_forward_chain() { output=$(iptables -L FORWARD -n --line-numbers) if ! echo "$output" | grep -q "^1[[:space:]]\+$NYM_CHAIN"; then - echo "FORWARD rule 1 is not ${NYM_CHAIN}; re-run network-tunnel-manager.sh exit_policy_install" + error "FORWARD rule 1 is not ${NYM_CHAIN}; re-run network-tunnel-manager.sh exit_policy_install" return 1 fi if ! echo "$output" | grep -q "ACCEPT.*state RELATED,ESTABLISHED"; then - echo "FORWARD chain missing RELATED,ESTABLISHED accepts; re-run network-tunnel-manager.sh apply_iptables_rules_wg" + error "FORWARD chain missing RELATED,ESTABLISHED accepts; re-run network-tunnel-manager.sh apply_iptables_rules_wg" return 1 fi - echo "FORWARD chain ordering looks good" + ok "FORWARD chain ordering looks good" return 0 } @@ -850,9 +850,9 @@ check_nym_exit_chain() { local line line=$(firewall_rule_line "$NYM_CHAIN" $((idx + 1))) if [[ "$line" =~ ${patterns[$idx]} ]]; then - echo "${NYM_CHAIN} rule $((idx + 1)) ok (${patterns[$idx]})" + ok "${NYM_CHAIN} rule $((idx + 1)) ok (${patterns[$idx]})" else - echo "${NYM_CHAIN} rule $((idx + 1)) is not ${patterns[$idx]}; re-run network-tunnel-manager.sh exit_policy_install" + error "${NYM_CHAIN} rule $((idx + 1)) is not ${patterns[$idx]}; re-run network-tunnel-manager.sh exit_policy_install" errors=1 fi done @@ -860,12 +860,12 @@ check_nym_exit_chain() { local last_rule last_rule=$(iptables -L "$NYM_CHAIN" -n --line-numbers | awk 'NR>2 {line=$0} END {print line}') if [[ -z "${last_rule:-}" ]]; then - echo "${NYM_CHAIN} chain is empty; re-run network-tunnel-manager.sh exit_policy_install" + error "${NYM_CHAIN} chain is empty; re-run network-tunnel-manager.sh exit_policy_install" errors=1 elif [[ "$last_rule" =~ REJECT ]] && [[ "$last_rule" =~ 0\.0\.0\.0/0 ]]; then - echo "${NYM_CHAIN} ends with the catch-all REJECT" + ok "${NYM_CHAIN} ends with the catch-all REJECT" else - echo "${NYM_CHAIN} final rule is not the catch-all REJECT (got: $last_rule)" + error "${NYM_CHAIN} final rule is not the catch-all REJECT (got: $last_rule)" errors=1 fi @@ -873,28 +873,28 @@ check_nym_exit_chain() { } check_firewall_setup() { - echo "checking ipv4 firewall ordering…" + info "checking ipv4 firewall ordering…" local errors=0 check_forward_chain || errors=1 check_nym_exit_chain || errors=1 if command -v ip6tables >/dev/null 2>&1; then - echo "checking ipv6 firewall ordering…" + info "checking ipv6 firewall ordering…" if ip6tables -L "$NYM_CHAIN" -n --line-numbers >/dev/null 2>&1; then if ! ip6tables -L "$NYM_CHAIN" -n --line-numbers | sed -n '3p' | grep -q "udp.*dpt:53"; then - echo "ip6tables ${NYM_CHAIN} rule 1 is not UDP 53" + error "ip6tables ${NYM_CHAIN} rule 1 is not UDP 53" errors=1 fi fi fi if [[ $errors -ne 0 ]]; then - echo "There may be some ordering issues, it is recommended to re-run network-tunnel-manager.sh exit_policy_install after configuring UFW." + error "There may be some ordering issues, it is recommended to re-run network-tunnel-manager.sh exit_policy_install after configuring UFW." return 1 fi - echo "It's looking good!" + ok "It's looking good!" return 0 } From 18d271f4811a7536055cfcc9774a95164275cc99 Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Thu, 20 Nov 2025 15:47:59 +0100 Subject: [PATCH 122/180] + colors test_exit_policy_connectivity --- .../nym-node-setup/network-tunnel-manager.sh | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index b4e6fdfb35..08a70dccbb 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -770,46 +770,46 @@ show_exit_policy_status() { } test_exit_policy_connectivity() { - echo "testing connectivity through $WG_INTERFACE" + info "testing connectivity through $WG_INTERFACE" local iface_info iface_info=$(ip link show "$WG_INTERFACE" 2>/dev/null || true) if [[ -z "$iface_info" ]]; then - echo "interface $WG_INTERFACE not found" + error "interface $WG_INTERFACE not found" return 1 fi - echo "interface:" - echo "$iface_info" + ok "interface:" + ok "$iface_info" local ipv4_address ipv6_address ipv4_address=$(ip -4 addr show dev "$WG_INTERFACE" | awk '/inet / {print $2}' | cut -d'/' -f1 | head -n1) ipv6_address=$(ip -6 addr show dev "$WG_INTERFACE" scope global | awk '/inet6/ {print $2}' | cut -d'/' -f1 | head -n1) - echo "ipv4 address: ${ipv4_address:-none}" - echo "ipv6 address: ${ipv6_address:-none}" + ok "ipv4 address: ${ipv4_address:-none}" + ok "ipv6 address: ${ipv6_address:-none}" if [[ -n "$ipv4_address" ]]; then - echo "testing ipv4 ping to 8.8.8.8" + info "testing ipv4 ping to 8.8.8.8" timeout 5 ping -c 3 -I "$ipv4_address" 8.8.8.8 >/dev/null 2>&1 && \ - echo "ipv4 ping ok" || echo "ipv4 ping failed" + ok "ipv4 ping ok" || error "ipv4 ping failed" - echo "testing ipv4 dns resolution" + info "testing ipv4 dns resolution" timeout 5 ping -c 3 -I "$ipv4_address" google.com >/dev/null 2>&1 && \ - echo "ipv4 dns ok" || echo "ipv4 dns failed" + ok "ipv4 dns ok" || error "ipv4 dns failed" fi if [[ -n "$ipv6_address" ]]; then - echo "testing ipv6 ping to google dns" + info "testing ipv6 ping to google dns" timeout 5 ping6 -c 3 -I "$ipv6_address" 2001:4860:4860::8888 >/dev/null 2>&1 && \ - echo "ipv6 ping ok" || echo "ipv6 ping failed" + ok "ipv6 ping ok" || error "ipv6 ping failed" - echo "testing ipv6 dns resolution" + info "testing ipv6 dns resolution" timeout 5 ping6 -c 3 -I "$ipv6_address" google.com >/dev/null 2>&1 && \ - echo "ipv6 dns ok" || echo "ipv6 dns failed" + ok "ipv6 dns ok" || error "ipv6 dns failed" fi - echo "connectivity tests finished" + info "connectivity tests finished" } From 7b96adf7a81121a60b28ce3ec63800fff305bd1a Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Thu, 20 Nov 2025 16:04:22 +0100 Subject: [PATCH 123/180] / refactor help section --- .../nym-node-setup/network-tunnel-manager.sh | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 08a70dccbb..abe0c1a804 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -1212,42 +1212,41 @@ case "$cmd" in usage: $0 [args] high level workflows: - full_tunnel_setup run tunnel iptables and checks for nymtun0 and nymwg - exit_policy_install install and configure wireguard exit policy - complete_networking_configuration run tunnel setup, exit policy install and tests + complete_networking_configuration Run tunnel setup, exit policy install and tests + exit_policy_install Install and configure wireguard exit policy + full_tunnel_setup Run tunnel iptables and checks for nymtun0 and nymwg tunnel and nat helpers: - fetch_ipv6_address_nym_tun show global ipv6 address on ${TUNNEL_INTERFACE} - fetch_and_display_ipv6 show ipv6 on uplink ${NETWORK_DEVICE} - apply_iptables_rules apply nat/forward rules for ${TUNNEL_INTERFACE} - apply_iptables_rules_wg apply nat/forward rules for ${WG_INTERFACE} - check_nymtun_iptables inspect forward chain for ${TUNNEL_INTERFACE} - check_nym_wg_tun inspect forward chain for ${WG_INTERFACE} - check_ipv6_ipv4_forwarding show ipv4/ipv6 forwarding flags - check_ip_routing show ipv4 and ipv6 routes - perform_pings test ipv4 and ipv6 pings - joke_through_the_mixnet test via ${TUNNEL_INTERFACE} with joke - joke_through_wg_tunnel test via ${WG_INTERFACE} with joke - configure_dns_and_icmp_wg allow ping and dns on this host - adjust_ip_forwarding enable ipv4/ipv6 forwarding via sysctl.d - remove_duplicate_rules deduplicate rules for interface in FORWARD and ${NYM_CHAIN} + adjust_ip_forwarding Enable ipv4/ipv6 forwarding via sysctl.d + apply_iptables_rules Apply nat/forward rules for ${TUNNEL_INTERFACE} + apply_iptables_rules_wg Apply nat/forward rules for ${WG_INTERFACE} + check_ip_routing Show ipv4 and ipv6 routes + check_ipv6_ipv4_forwarding Show ipv4/ipv6 forwarding flags + 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} + 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 + perform_pings Test ipv4 and ipv6 pings + remove_duplicate_rules Deduplicate FORWARD and ${NYM_CHAIN} rules for (required). exit policy manager: - exit_policy_install install exit policy (iptables rules and blocklist) - check_firewall_setup run ordering sanity check (dns/icmp + FORWARD jump) - exit_policy_status show status of exit policy and forwarding - exit_policy_test_connectivity test connectivity via ${WG_INTERFACE} - exit_policy_clear remove ${NYM_CHAIN} chains and hooks + check_firewall_setup Run ordering sanity check (dns/icmp + FORWARD jump) + exit_policy_clear Remove ${NYM_CHAIN} chains and hooks + exit_policy_install Install exit policy (iptables rules and blocklist) + exit_policy_status Show status of exit policy and forwarding + exit_policy_test_connectivity Test connectivity via ${WG_INTERFACE} exit_policy_tests [--skip-default-reject] - run verification tests on exit policy + Run verification tests on exit policy (options: --skip-default-reject). environment overrides: - TUNNEL_INTERFACE default nymtun0 - WG_INTERFACE default nymwg - NETWORK_DEVICE uplink device, auto-detected if not set + 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. EOF - status=0 ;; *) From 9b076197b1c7604b381a1e4f0ebb619ad63ecae7 Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Thu, 20 Nov 2025 16:04:22 +0100 Subject: [PATCH 124/180] / refactor help section --- .../nym-node-setup/network-tunnel-manager.sh | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 7017adbd4d..2096fd836b 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -1103,41 +1103,41 @@ case "$cmd" in usage: $0 [args] high level workflows: - full_tunnel_setup run tunnel iptables and checks for nymtun0 and nymwg - exit_policy_install install and configure wireguard exit policy - complete_networking_configuration run tunnel setup, exit policy install and tests + complete_networking_configuration Run tunnel setup, exit policy install and tests + exit_policy_install Install and configure wireguard exit policy + full_tunnel_setup Run tunnel iptables and checks for nymtun0 and nymwg tunnel and nat helpers: - fetch_ipv6_address_nym_tun show global ipv6 address on ${TUNNEL_INTERFACE} - fetch_and_display_ipv6 show ipv6 on uplink ${NETWORK_DEVICE} - apply_iptables_rules apply nat/forward rules for ${TUNNEL_INTERFACE} - apply_iptables_rules_wg apply nat/forward rules for ${WG_INTERFACE} - check_nymtun_iptables inspect forward chain for ${TUNNEL_INTERFACE} - check_nym_wg_tun inspect forward chain for ${WG_INTERFACE} - check_ipv6_ipv4_forwarding show ipv4/ipv6 forwarding flags - check_ip_routing show ipv4 and ipv6 routes - perform_pings test ipv4 and ipv6 pings - joke_through_the_mixnet test via ${TUNNEL_INTERFACE} with joke - joke_through_wg_tunnel test via ${WG_INTERFACE} with joke - configure_dns_and_icmp_wg allow ping and dns on this host - adjust_ip_forwarding enable ipv4/ipv6 forwarding via sysctl.d - remove_duplicate_rules deduplicate rules for interface in FORWARD and ${NYM_CHAIN} + adjust_ip_forwarding Enable ipv4/ipv6 forwarding via sysctl.d + apply_iptables_rules Apply nat/forward rules for ${TUNNEL_INTERFACE} + apply_iptables_rules_wg Apply nat/forward rules for ${WG_INTERFACE} + check_ip_routing Show ipv4 and ipv6 routes + check_ipv6_ipv4_forwarding Show ipv4/ipv6 forwarding flags + 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} + 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 + perform_pings Test ipv4 and ipv6 pings + remove_duplicate_rules Deduplicate FORWARD and ${NYM_CHAIN} rules for (required). exit policy manager: - exit_policy_install install exit policy (iptables rules and blocklist) - exit_policy_status show status of exit policy and forwarding - exit_policy_test_connectivity test connectivity via ${WG_INTERFACE} - exit_policy_clear remove ${NYM_CHAIN} chains and hooks + check_firewall_setup Run ordering sanity check (dns/icmp + FORWARD jump) + exit_policy_clear Remove ${NYM_CHAIN} chains and hooks + exit_policy_install Install exit policy (iptables rules and blocklist) + exit_policy_status Show status of exit policy and forwarding + exit_policy_test_connectivity Test connectivity via ${WG_INTERFACE} exit_policy_tests [--skip-default-reject] - run verification tests on exit policy + Run verification tests on exit policy (options: --skip-default-reject). environment overrides: - TUNNEL_INTERFACE default nymtun0 - WG_INTERFACE default nymwg - NETWORK_DEVICE uplink device, auto-detected if not set + 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. EOF - status=0 ;; *) From 50d768976fba54a80f078a5e9dea6eb67c014d73 Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Thu, 20 Nov 2025 16:20:15 +0100 Subject: [PATCH 125/180] end status of help --- scripts/nym-node-setup/network-tunnel-manager.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index abe0c1a804..38215258c6 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -1247,6 +1247,7 @@ environment overrides: WG_INTERFACE Default: nymwg - Must match your WireGuard interface name. EOF + status=0 ;; *) @@ -1256,7 +1257,7 @@ EOF ;; esac -if [ "${status:-1}" -eq 0 ]; then +if [[ "$cmd" != help && "$cmd" != "--help" && "$cmd" != "-h" && ${status:-1} -eq 0 ]]; then echo "operation ${cmd} completed" fi exit $status From f9e2311574630d5b3eb6e5abdf5c9bf498784157 Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Thu, 20 Nov 2025 16:20:15 +0100 Subject: [PATCH 126/180] end status of help --- scripts/nym-node-setup/network-tunnel-manager.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 2096fd836b..bd97153db1 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -1138,6 +1138,7 @@ environment overrides: WG_INTERFACE Default: nymwg - Must match your WireGuard interface name. EOF + status=0 ;; *) @@ -1147,7 +1148,7 @@ EOF ;; esac -if [ "${status:-1}" -eq 0 ]; then +if [[ "$cmd" != help && "$cmd" != "--help" && "$cmd" != "-h" && ${status:-1} -eq 0 ]]; then echo "operation ${cmd} completed" fi exit $status From bb5b43492a34f82ed7428a4b404b73d42771a13b Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Thu, 20 Nov 2025 16:28:05 +0100 Subject: [PATCH 127/180] + colors show_exit_policy_status --- .../nym-node-setup/network-tunnel-manager.sh | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 38215258c6..72cbf927c7 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -740,31 +740,31 @@ clear_exit_policy_rules() { } show_exit_policy_status() { - echo "nym exit policy status" - echo "network device: $NETWORK_DEVICE" - echo "wireguard interface: $WG_INTERFACE" + info "nym exit policy status" + info "network device: ${NC}$NETWORK_DEVICE" + info "wireguard interface: ${NC}$WG_INTERFACE" echo if ! ip link show "$WG_INTERFACE" >/dev/null 2>&1; then - echo "warning: wireguard interface $WG_INTERFACE not found" + error "warning: wireguard interface $WG_INTERFACE not found" else - echo "interface details:" + info "interface details:" ip link show "$WG_INTERFACE" echo - echo "ipv4 addresses:" + info "ipv4 addresses:" ip -4 addr show dev "$WG_INTERFACE" echo - echo "ipv6 addresses:" + info "ipv6 addresses:" ip -6 addr show dev "$WG_INTERFACE" fi echo - echo "iptables chains for ${NYM_CHAIN}:" + info "iptables chains for ${NYM_CHAIN}:" iptables -L "$NYM_CHAIN" -n -v 2>/dev/null || echo "ipv4 chain not found" echo ip6tables -L "$NYM_CHAIN" -n -v 2>/dev/null || echo "ipv6 chain not found" echo - echo "ip forwarding:" + info "ip forwarding:" echo "ipv4: $(cat /proc/sys/net/ipv4/ip_forward 2>/dev/null || echo 0)" echo "ipv6: $(cat /proc/sys/net/ipv6/conf/all/forwarding 2>/dev/null || echo 0)" } @@ -1251,13 +1251,13 @@ EOF ;; *) - echo "unknown command: $cmd" - echo "run with 'help' for usage" + error "unknown command: $cmd" + info "run with 'help' for usage" exit 1 ;; esac if [[ "$cmd" != help && "$cmd" != "--help" && "$cmd" != "-h" && ${status:-1} -eq 0 ]]; then - echo "operation ${cmd} completed" + ok "operation ${cmd} completed" fi exit $status From a488a1b4899316f9af6a6868ce8da4f6b9a4f510 Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Thu, 20 Nov 2025 16:50:05 +0100 Subject: [PATCH 128/180] / color fixes --- .../nym-node-setup/network-tunnel-manager.sh | 158 +++++++++--------- 1 file changed, 77 insertions(+), 81 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 72cbf927c7..3d047b7601 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -95,16 +95,16 @@ ensure_jq() { install_iptables_persistent() { if ! dpkg -s iptables-persistent >/dev/null 2>&1; then - echo -e "${YELLOW}installing iptables-persistent${NC}" + info "installing iptables-persistent" apt-get update -y DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent else - echo -e "${GREEN}iptables-persistent is already installed${NC}" + ok "iptables-persistent is already installed" fi } adjust_ip_forwarding() { - echo -e "${YELLOW}configuring ip forwarding via /etc/sysctl.d/99-nym-forwarding.conf${NC}" + info "configuring ip forwarding via /etc/sysctl.d/99-nym-forwarding.conf" install -m 0644 /dev/null /etc/sysctl.d/99-nym-forwarding.conf cat > /etc/sysctl.d/99-nym-forwarding.conf </dev/null || echo 0) if [[ "$v4" == "1" && "$v6" == "1" ]]; then - echo -e "${GREEN}ipv4 and ipv6 forwarding enabled${NC}" + ok "ipv4 and ipv6 forwarding enabled" else - echo -e "${RED}warning: ip forwarding not fully enabled (ipv4=$v4 ipv6=$v6)${NC}" + error "warning: ip forwarding not fully enabled (ipv4=$v4 ipv6=$v6)" fi } save_iptables_rules() { - echo -e "${YELLOW}saving iptables rules to /etc/iptables${NC}" + info "saving iptables rules to /etc/iptables$" mkdir -p /etc/iptables iptables-save > /etc/iptables/rules.v4 ip6tables-save > /etc/iptables/rules.v6 - echo -e "${GREEN}iptables rules saved${NC}" + ok "iptables rules saved" } ############################################################################### @@ -141,10 +141,10 @@ fetch_ipv6_address() { ipv6_global_address=$(ip -6 addr show "$interface" scope global | awk '/inet6/ {print $2}' | head -n 1) if [[ -z "$ipv6_global_address" ]]; then - echo "no globally routable ipv6 address found on $interface. please configure ipv6 or check your network settings" + error "no globally routable ipv6 address found on $interface. please configure ipv6 or check your network settings" exit 1 else - echo "using ipv6 address: $ipv6_global_address" + info "using ipv6 address: $ipv6_global_address" fi } @@ -152,9 +152,9 @@ fetch_and_display_ipv6() { local ipv6_address ipv6_address=$(ip -6 addr show "$NETWORK_DEVICE" scope global | awk '/inet6/ {print $2}') if [[ -z "$ipv6_address" ]]; then - echo "no global ipv6 address found on $NETWORK_DEVICE" + error "no global ipv6 address found on $NETWORK_DEVICE" else - echo "ipv6 address on $NETWORK_DEVICE: $ipv6_address" + ok "ipv6 address on $NETWORK_DEVICE: $ipv6_address" fi } @@ -165,11 +165,11 @@ remove_duplicate_rules() { local interface="$1" if [[ -z "$interface" ]]; then - echo "Error: No interface specified. Usage: $0 remove_duplicate_rules " + error "Error: No interface specified. Usage: $0 remove_duplicate_rules " exit 1 fi - echo "detecting and removing duplicate rules for $interface in FORWARD and ${NYM_CHAIN}" + info "detecting and removing duplicate rules for $interface in FORWARD and ${NYM_CHAIN}" # # ipv4 @@ -178,7 +178,7 @@ remove_duplicate_rules() { rules_v4=$(iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep -F -- "$interface" || true) if [[ -n "$rules_v4" ]]; then - echo "processing ipv4 rules" + info "processing ipv4 rules" local tmp4 tmp4=$(mktemp) @@ -192,7 +192,7 @@ remove_duplicate_rules() { count=$(printf "%s\n" "$rules_v4" | grep -F -- "$rule" | wc -l) if [[ "$count" -gt 1 ]]; then - echo "removing $((count - 1)) duplicate(s) of ipv4 rule: $rule" + info "removing $((count - 1)) duplicate(s) of ipv4 rule: $rule" for ((i=1; i/dev/null || \ - echo "warning: failed deleting ipv4 duplicate via index ($chain $index)" + error "warning: failed deleting ipv4 duplicate via index ($chain $index)" else - echo "warning: unable to locate ipv4 duplicate index for: $rule" + error "warning: unable to locate ipv4 duplicate index for: $rule" fi else - echo "warning: could not reliably match ipv4 duplicate rule: $rule" + error "warning: could not reliably match ipv4 duplicate rule: $rule" fi done fi @@ -228,7 +228,7 @@ remove_duplicate_rules() { rm -f "$tmp4" else - echo "no ipv4 rules found for $interface to deduplicate" + ok "no ipv4 rules found for $interface to deduplicate" fi @@ -240,7 +240,7 @@ remove_duplicate_rules() { rules_v6=$(ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep -F -- "$interface" || true) if [[ -n "$rules_v6" ]]; then - echo "processing ipv6 rules" + info "processing ipv6 rules" local tmp6 tmp6=$(mktemp) @@ -254,7 +254,7 @@ remove_duplicate_rules() { count=$(printf "%s\n" "$rules_v6" | grep -F -- "$rule" | wc -l) if [[ "$count" -gt 1 ]]; then - echo "removing $((count - 1)) duplicate(s) of ipv6 rule: $rule" + info "removing $((count - 1)) duplicate(s) of ipv6 rule: $rule" for ((i=1; i/dev/null || \ - echo "warning: failed deleting ipv6 duplicate via index ($chain $index)" + error "warning: failed deleting ipv6 duplicate via index ($chain $index)" else - echo "warning: unable to locate ipv6 duplicate index for: $rule" + error "warning: unable to locate ipv6 duplicate index for: $rule" fi else - echo "warning: could not match ipv6 duplicate rule reliably: $rule" + error "warning: could not match ipv6 duplicate rule reliably: $rule" fi done fi @@ -291,15 +291,15 @@ remove_duplicate_rules() { rm -f "$tmp6" else - echo "no ipv6 rules found for $interface to deduplicate" + error "no ipv6 rules found for $interface to deduplicate" fi - echo "duplicate rule scan completed for $interface" + ok "duplicate rule scan completed for $interface" } apply_iptables_rules() { local interface=$1 - echo "applying iptables rules for $interface using uplink $NETWORK_DEVICE" + info "applying iptables rules for $interface using uplink $NETWORK_DEVICE" sleep 1 # ipv4 nat and forwarding @@ -327,11 +327,11 @@ apply_iptables_rules() { check_tunnel_iptables() { local interface=$1 - echo "inspecting iptables rules for $interface" - echo "ipv4 forward chain:" + info "inspecting iptables rules for $interface" + info "ipv4 forward chain:" iptables -L FORWARD -v -n | awk -v dev="$interface" '/^Chain FORWARD/ || $0 ~ dev || $0 ~ "ufw-reject-forward"' echo - echo "ipv6 forward chain:" + info "ipv6 forward chain:" ip6tables -L FORWARD -v -n | awk -v dev="$interface" '/^Chain FORWARD/ || $0 ~ dev || $0 ~ "ufw6-reject-forward"' } @@ -339,100 +339,96 @@ check_ipv6_ipv4_forwarding() { local result_ipv4 result_ipv6 result_ipv4=$(cat /proc/sys/net/ipv4/ip_forward) result_ipv6=$(cat /proc/sys/net/ipv6/conf/all/forwarding) - echo "ipv4 forwarding is $([ "$result_ipv4" == "1" ] && echo enabled || echo not enabled)" - echo "ipv6 forwarding is $([ "$result_ipv6" == "1" ] && echo enabled || echo not enabled)" + ok "ipv4 forwarding is $([ "$result_ipv4" == "1" ] && ok enabled || error not enabled)" + ok "ipv6 forwarding is $([ "$result_ipv6" == "1" ] && ok enabled || error not enabled)" } check_ip_routing() { - echo "ipv4 routing table:" + info "ipv4 routing table:" ip route - echo "---------------------------" - echo "ipv6 routing table:" + info "---------------------------" + info "ipv6 routing table:" ip -6 route } perform_pings() { - echo "performing ipv4 ping to google.com" - ping -c 4 google.com || echo "ipv4 ping failed" + info "performing ipv4 ping to google.com" + ping -c 4 google.com || error "ipv4 ping failed" echo "---------------------------" - echo "performing ipv6 ping to google.com" - ping6 -c 4 google.com || echo "ipv6 ping failed" + info "performing ipv6 ping to google.com" + ping6 -c 4 google.com || error "ipv6 ping failed" } joke_through_tunnel() { ensure_jq local interface=$1 - local green="\033[0;32m" - local reset="\033[0m" - local red="\033[0;31m" - local yellow="\033[0;33m" sleep 1 echo - echo -e "${yellow}checking tunnel connectivity and fetching a joke for $interface${reset}" - echo -e "${yellow}if this test succeeds, it confirms your machine can reach the outside world via ipv4 and ipv6${reset}" - echo -e "${yellow}probes and external clients may still see different connectivity to your nym node${reset}" + info "checking tunnel connectivity and fetching a joke for $interface" + info "if this test succeeds, it confirms your machine can reach the outside world via ipv4 and ipv6" + info "probes and external clients may still see different connectivity to your nym node" local ipv4_address ipv6_address joke ipv4_address=$(ip addr show "$interface" | awk '/inet / {print $2}' | cut -d'/' -f1) ipv6_address=$(ip addr show "$interface" | awk '/inet6 / && $2 !~ /^fe80/ {print $2}' | cut -d'/' -f1) if [[ -z "$ipv4_address" && -z "$ipv6_address" ]]; then - echo -e "${red}no ip address found on $interface. unable to fetch a joke${reset}" - echo -e "${red}please verify your tunnel configuration and ensure the interface is up${reset}" + error "no ip address found on $interface. unable to fetch a joke" + error "please verify your tunnel configuration and ensure the interface is up" return 1 fi if [[ -n "$ipv4_address" ]]; then echo echo "------------------------------------" - echo "detected ipv4 address: $ipv4_address" - echo "testing ipv4 connectivity" + info "detected ipv4 address: $ipv4_address" + info "testing ipv4 connectivity" echo if ping -c 1 -I "$ipv4_address" google.com >/dev/null 2>&1; then - echo -e "${green}ipv4 connectivity is working. fetching a joke${reset}" + ok "ipv4 connectivity is working. fetching a joke" joke=$(curl -s -H "Accept: application/json" --interface "$ipv4_address" https://icanhazdadjoke.com/ | jq -r .joke) - [[ -n "$joke" && "$joke" != "null" ]] && echo -e "${green}ipv4 joke: $joke${reset}" || echo "failed to fetch a joke via ipv4" + [[ -n "$joke" && "$joke" != "null" ]] && ok "ipv4 joke: $joke" || echo "failed to fetch a joke via ipv4" else - echo -e "${red}ipv4 connectivity is not working for $interface. verify your routing and nat settings${reset}" + error "ipv4 connectivity is not working for $interface. verify your routing and nat settings" fi else - echo -e "${red}no ipv4 address found on $interface. unable to fetch a joke via ipv4${reset}" + error "no ipv4 address found on $interface. unable to fetch a joke via ipv4" fi if [[ -n "$ipv6_address" ]]; then echo echo "------------------------------------" - echo "detected ipv6 address: $ipv6_address" - echo "testing ipv6 connectivity" + info "detected ipv6 address: $ipv6_address" + info "testing ipv6 connectivity" echo if ping6 -c 1 -I "$ipv6_address" google.com >/dev/null 2>&1; then - echo -e "${green}ipv6 connectivity is working. fetching a joke${reset}" + ok "ipv6 connectivity is working. fetching a joke" joke=$(curl -s -H "Accept: application/json" --interface "$ipv6_address" https://icanhazdadjoke.com/ | jq -r .joke) - [[ -n "$joke" && "$joke" != "null" ]] && echo -e "${green}ipv6 joke: $joke${reset}" || echo -e "${red}failed to fetch a joke via ipv6${reset}" + [[ -n "$joke" && "$joke" != "null" ]] && ok "ipv6 joke: $joke" || error "failed to fetch a joke via ipv6" else - echo -e "${red}ipv6 connectivity is not working for $interface. verify your routing and nat settings${reset}" + error "ipv6 connectivity is not working for $interface. verify your routing and nat settings" fi else - echo -e "${red}no ipv6 address found on $interface. unable to fetch a joke via ipv6${reset}" + error "no ipv6 address found on $interface. unable to fetch a joke via ipv6" fi - echo -e "${green}joke fetching processes completed for $interface${reset}" + ok "joke fetching processes completed for $interface" echo "------------------------------------" sleep 3 echo echo - echo -e "${yellow}connectivity testing recommendations${reset}" - echo -e "${yellow}- from another machine use wscat to test websocket connectivity on 9001${reset}" - echo -e "${yellow}- test udp connectivity on port 51822 (wireguard)${reset}" - echo -e "${yellow}- example: echo 'test' | nc -u 51822${reset}" + info "connectivity testing recommendations" + info "- from another machine use wscat to test websocket connectivity on 9001" + info "- test udp connectivity on port 51822 (wireguard)" + info "- example: echo 'test' | nc -u 51822" } configure_dns_and_icmp_wg() { - echo "allowing ping (icmp) and dns on this host" + info "allowing ping (icmp) and dns on this host" iptables -C INPUT -p icmp --icmp-type echo-request -j ACCEPT 2>/dev/null || \ iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT iptables -C OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT 2>/dev/null || \ @@ -444,7 +440,7 @@ configure_dns_and_icmp_wg() { iptables -A INPUT -p tcp --dport 53 -j ACCEPT save_iptables_rules - echo "dns and icmp configuration completed" + ok "dns and icmp configuration completed" } ############################################################################### @@ -463,12 +459,12 @@ add_port_rules() { if ! $cmd -C "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT 2>/dev/null; then $cmd -A "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT - echo "added $cmd $NYM_CHAIN $protocol port range $start_port:$end_port" + ok "added $cmd $NYM_CHAIN $protocol port range $start_port:$end_port" fi else if ! $cmd -C "$NYM_CHAIN" -p "$protocol" --dport "$port" -j ACCEPT 2>/dev/null; then $cmd -A "$NYM_CHAIN" -p "$protocol" --dport "$port" -j ACCEPT - echo "added $cmd $NYM_CHAIN $protocol port $port" + ok "added $cmd $NYM_CHAIN $protocol port $port" fi fi } @@ -478,14 +474,14 @@ exit_policy_install_deps() { for cmd in iptables ip6tables ip grep sed awk wget curl; do if ! command -v "$cmd" >/dev/null 2>&1; then - echo "installing dependency: $cmd" + info "installing dependency: $cmd" apt-get install -y "$cmd" fi done } create_nym_chain() { - echo "creating nym exit policy chain $NYM_CHAIN" + info "creating nym exit policy chain $NYM_CHAIN" if iptables -L "$NYM_CHAIN" >/dev/null 2>&1; then iptables -F "$NYM_CHAIN" @@ -509,7 +505,7 @@ create_nym_chain() { } setup_nat_rules() { - echo "setting up nat and forwarding rules for $WG_INTERFACE via $NETWORK_DEVICE" + info "setting up nat and forwarding rules for $WG_INTERFACE via $NETWORK_DEVICE" 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 @@ -534,7 +530,7 @@ setup_nat_rules() { } configure_exit_dns_and_icmp() { - echo "ensuring dns and icmp are allowed inside nym exit chain" + info "ensuring dns and icmp are allowed inside nym exit chain" # Remove any existing DNS/ICMP rules first to avoid duplicates iptables -D "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT 2>/dev/null || true @@ -727,7 +723,7 @@ add_default_reject_rule() { } clear_exit_policy_rules() { - echo "clearing nym exit policy rules" + info "clearing nym exit policy rules ..." iptables -F "$NYM_CHAIN" 2>/dev/null || true ip6tables -F "$NYM_CHAIN" 2>/dev/null || true @@ -741,8 +737,8 @@ clear_exit_policy_rules() { show_exit_policy_status() { info "nym exit policy status" - info "network device: ${NC}$NETWORK_DEVICE" - info "wireguard interface: ${NC}$WG_INTERFACE" + info "network device: $NETWORK_DEVICE" + info "wireguard interface: $WG_INTERFACE" echo if ! ip link show "$WG_INTERFACE" >/dev/null 2>&1; then @@ -790,26 +786,26 @@ test_exit_policy_connectivity() { ok "ipv6 address: ${ipv6_address:-none}" if [[ -n "$ipv4_address" ]]; then - info "testing ipv4 ping to 8.8.8.8" + echo -e "${NC}testing ipv4 ping to 8.8.8.8 ..." timeout 5 ping -c 3 -I "$ipv4_address" 8.8.8.8 >/dev/null 2>&1 && \ ok "ipv4 ping ok" || error "ipv4 ping failed" - info "testing ipv4 dns resolution" + echo -e "${NC}testing ipv4 dns resolution ..." timeout 5 ping -c 3 -I "$ipv4_address" google.com >/dev/null 2>&1 && \ ok "ipv4 dns ok" || error "ipv4 dns failed" fi if [[ -n "$ipv6_address" ]]; then - info "testing ipv6 ping to google dns" + echo -e "${NC}testing ipv6 ping to google dns ..." timeout 5 ping6 -c 3 -I "$ipv6_address" 2001:4860:4860::8888 >/dev/null 2>&1 && \ ok "ipv6 ping ok" || error "ipv6 ping failed" - info "testing ipv6 dns resolution" + echo -e "${NC}testing ipv6 dns resolution ..." timeout 5 ping6 -c 3 -I "$ipv6_address" google.com >/dev/null 2>&1 && \ ok "ipv6 dns ok" || error "ipv6 dns failed" fi - info "connectivity tests finished" + ok "connectivity tests finished" } From ef7974fde97927bc26cafc4d8765d57b88c915c4 Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Thu, 20 Nov 2025 17:29:42 +0100 Subject: [PATCH 129/180] / otpimize create_nym_chain --- scripts/nym-node-setup/network-tunnel-manager.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 3d047b7601..41b9c3c7f2 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -483,13 +483,13 @@ exit_policy_install_deps() { create_nym_chain() { info "creating nym exit policy chain $NYM_CHAIN" - if iptables -L "$NYM_CHAIN" >/dev/null 2>&1; then + if iptables -S "$NYM_CHAIN" >/dev/null 2>&1; then iptables -F "$NYM_CHAIN" else iptables -N "$NYM_CHAIN" fi - if ip6tables -L "$NYM_CHAIN" >/dev/null 2>&1; then + if ip6tables -S "$NYM_CHAIN" >/dev/null 2>&1; then ip6tables -F "$NYM_CHAIN" else ip6tables -N "$NYM_CHAIN" From b00e1f2fffa05f0bc24f28b134bddba69f1028ed Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Thu, 20 Nov 2025 17:32:57 +0100 Subject: [PATCH 130/180] addressing comment --- .../nym-node-status-api/src/http/models.rs | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/nym-node-status-api/nym-node-status-api/src/http/models.rs b/nym-node-status-api/nym-node-status-api/src/http/models.rs index 3d4b9fbf68..2f0fe09183 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/models.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/models.rs @@ -372,6 +372,21 @@ impl DVpnGateway { } } +struct NodeScore { + download_speed_score: f64, + ping_ips_score: f64, + mixnet_performance: f64, +} + +impl NodeScore { + // Weighted scoring: mixnet performance (40%), download speed (30%), ping performance (30%) + fn calculate_weighted_score(&self) -> f64 { + (self.mixnet_performance * 0.4) + + (self.download_speed_score * 0.3) + + (self.ping_ips_score * 0.3) + } +} + /// calculates the gateway probe score for mixnet mode fn calculate_mixnet_score(gateway: &Gateway) -> ScoreValue { let mixnet_performance = gateway.performance as f64 / 100.0; @@ -391,7 +406,7 @@ fn calculate_mixnet_score(gateway: &Gateway) -> ScoreValue { fn calculate_score(gateway: &Gateway, probe_outcome: &LastProbeResult) -> ScoreValue { let mixnet_performance = gateway.performance as f64 / 100.0; - let (download_speed_score, ping_ips_performance) = probe_outcome + let node_score = probe_outcome .outcome .wg .as_ref() @@ -420,13 +435,19 @@ fn calculate_score(gateway: &Gateway, probe_outcome: &LastProbeResult) -> ScoreV 0.1 }; - (file_download_score, ping_ips_performance) + NodeScore { + download_speed_score: file_download_score, + ping_ips_score: ping_ips_performance, + mixnet_performance, + } }) - .unwrap_or((0.0, 0.0)); + .unwrap_or(NodeScore { + download_speed_score: 0.0, + ping_ips_score: 0.0, + mixnet_performance, + }); - // Weighted scoring: mixnet performance (40%), download speed (30%), ping performance (30%) - let weighted_score = - (mixnet_performance * 0.4) + (download_speed_score * 0.3) + (ping_ips_performance * 0.3); + let weighted_score = node_score.calculate_weighted_score(); if weighted_score > 0.75 { ScoreValue::High From 76993a9b940405237c930cb3e767d016ee2ea8d8 Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Thu, 20 Nov 2025 17:36:27 +0100 Subject: [PATCH 131/180] / colors --- scripts/nym-node-setup/network-tunnel-manager.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 41b9c3c7f2..ca68f1593c 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -657,12 +657,12 @@ apply_port_allowlist() { } apply_spamhaus_blocklist() { - echo "applying spamhaus-like blocklist from $EXIT_POLICY_LOCATION" + info "applying spamhaus-like blocklist from $EXIT_POLICY_LOCATION" mkdir -p "$(dirname "$POLICY_FILE")" if ! wget -q "$EXIT_POLICY_LOCATION" -O "$POLICY_FILE" 2>/dev/null; then - echo "failed to download exit policy, using minimal blocklist" + arror "failed to download exit policy, using minimal blocklist" cat >"$POLICY_FILE" </dev/null; then iptables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT --reject-with icmp-port-unreachable \ - || echo "warning: failed adding ipv4 reject for $ip_range" + || error "warning: failed adding ipv4 reject for $ip_range" fi # ipv6 reject if [[ "$ip_range" == *":"* ]]; then if ! ip6tables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT 2>/dev/null; then ip6tables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT \ - || echo "warning: failed adding ipv6 reject for $ip_range" + || error "warning: failed adding ipv6 reject for $ip_range" fi fi @@ -711,7 +711,7 @@ EOF add_default_reject_rule() { - echo "ensuring default reject rule at end of ${NYM_CHAIN}" + info "ensuring default reject rule at end of ${NYM_CHAIN}" iptables -D "$NYM_CHAIN" -j REJECT 2>/dev/null || true iptables -D "$NYM_CHAIN" -j REJECT --reject-with icmp-port-unreachable 2>/dev/null || true @@ -1038,7 +1038,7 @@ exit_policy_run_tests() { ((total += 1)) fi - echo "tests run: $total, test failed: $failed" + info "tests run: ${GREEN}$total${YELLOW}, test failed: ${RED}$failed${NC}" if [[ $failed -eq 0 ]]; then ok "all exit policy tests passed" else From 54c7a0148279384650e1b1d1b9e07d77ff3387a4 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Thu, 20 Nov 2025 17:36:30 +0100 Subject: [PATCH 132/180] update version --- Cargo.lock | 2 +- nym-node-status-api/nym-node-status-api/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ec3531bb6..54a16eafe2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6552,7 +6552,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "4.0.11-rc1" +version = "4.0.11-testing" dependencies = [ "ammonia", "anyhow", diff --git a/nym-node-status-api/nym-node-status-api/Cargo.toml b/nym-node-status-api/nym-node-status-api/Cargo.toml index c92ebb4dd9..b32100a6ba 100644 --- a/nym-node-status-api/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/nym-node-status-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-api" -version = "4.0.11-rc1" +version = "4.0.11-testing" authors.workspace = true repository.workspace = true homepage.workspace = true From 6170ca2a1413c58b570bef66e1e8f508570040cd Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Fri, 21 Nov 2025 11:04:48 +0000 Subject: [PATCH 133/180] Update time-now.md From 6d63ba1f4cafff42ec7e60b0af3e43680a40f454 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:17:50 +0100 Subject: [PATCH 134/180] menu fix --- scripts/nym-node-setup/network-tunnel-manager.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index ca68f1593c..3072110181 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -1208,9 +1208,9 @@ case "$cmd" in usage: $0 [args] high level workflows: - complete_networking_configuration Run tunnel setup, exit policy install and tests + complete_networking_configuration Install tunnel interfaces, setup networking, iptables & wg exit policy exit_policy_install Install and configure wireguard exit policy - full_tunnel_setup Run tunnel iptables and checks for nymtun0 and nymwg + full_tunnel_setup Install tunnel interfaces & setup networking tunnel and nat helpers: adjust_ip_forwarding Enable ipv4/ipv6 forwarding via sysctl.d From 52f98de73b571748054f4b4013f1f0e5bbb337c8 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:40:24 +0100 Subject: [PATCH 135/180] simplify --- scripts/nym-node-setup/network-tunnel-manager.sh | 2 +- scripts/nym-node-setup/nym-node-cli.py | 16 ++-------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 3072110181..3d60cc5bfb 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -1208,7 +1208,7 @@ case "$cmd" in usage: $0 [args] high level workflows: - complete_networking_configuration Install tunnel interfaces, setup networking, iptables & wg exit policy + complete_networking_configuration Install tunnel interfaces, setup networking, iptables, wg exit policy & tests exit_policy_install Install and configure wireguard exit policy full_tunnel_setup Install tunnel interfaces & setup networking diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index def82b1d17..9d99fc4ad0 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -345,25 +345,13 @@ class NodeSetupCLI: """A standalone fn to pass full cmd list needed for correct setup and test network tunneling, using an external script""" print( "\n* * * Setting up network configuration for mixnet IP router and Wireguard tunneling * * *" - "\nMore info: https://nym.com/docs/operators/nodes/nym-node/configuration#1-download-network_tunnel_managersh-make-executable-and-run" + "\nMore info: https://nym.com/docs/operators/nodes/nym-node/configuration#routing-configuration" "\nThis may take a while; follow the steps below and don't kill the process..." ) # each entry is the exact argv to pass to the script steps = [ - ["check_nymtun_iptables"], - ["remove_duplicate_rules", "nymtun0"], - ["remove_duplicate_rules", "nymwg"], - ["check_nymtun_iptables"], - ["adjust_ip_forwarding"], - ["apply_iptables_rules"], - ["check_nymtun_iptables"], - ["apply_iptables_rules_wg"], - ["configure_dns_and_icmp_wg"], - ["adjust_ip_forwarding"], - ["check_ipv6_ipv4_forwarding"], - ["joke_through_the_mixnet"], - ["joke_through_wg_tunnel"], + ["complete_networking_configuration"] ] for argv in steps: From 28dc7cae4db58570900609a349f3336e88c99f57 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Mon, 24 Nov 2025 11:40:22 +0100 Subject: [PATCH 136/180] add logging and logfile --- .../nym-node-setup/network-tunnel-manager.sh | 50 ++++++++++++++++- .../nym-node-setup/quic_bridge_deployment.sh | 55 ++++++++++++------- 2 files changed, 83 insertions(+), 22 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 3d60cc5bfb..af14f62912 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -3,6 +3,8 @@ # run this script as root set -euo pipefail +set +o errtrace + ############################################################################### # colors (no emojis) @@ -11,6 +13,8 @@ GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[0;33m' NC='\033[0m' +CYAN='\033[0;36m' +RESET='\033[0m' info() { printf "%b\n" "${YELLOW}[INFO] $*${NC}" @@ -32,6 +36,44 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi +############################################################################### +# Logging +############################################################################### +LOG_FILE="/var/log/nym/network_tunnel_manager.log" +mkdir -p "$(dirname "$LOG_FILE")" +touch "$LOG_FILE" +chmod 640 "$LOG_FILE" + +# rotate log if >10MB +if [[ -f "$LOG_FILE" && $(stat -c%s "$LOG_FILE") -gt 10485760 ]]; then + mv "$LOG_FILE" "${LOG_FILE}.1" + touch "$LOG_FILE" + chmod 640 "$LOG_FILE" +fi + +echo "----- $(date '+%Y-%m-%d %H:%M:%S') START network-tunnel-manager -----" | tee -a "$LOG_FILE" +echo -e "${CYAN}Logs are being saved locally to:${RESET} $LOG_FILE" +echo -e "${CYAN}These logs never leave your machine.${RESET}" +echo "" | tee -a "$LOG_FILE" + +# safe logger +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" +} + +# simple redirection that keeps function scope intact +add_log_redirection() { + exec > >(sed -u 's/\x1b\[[0-9;]*m//g' | tee -a "$LOG_FILE") 2>&1 +} +add_log_redirection + +trap 'log "ERROR: exit=$? line=$LINENO cmd=$(printf "%q" "$BASH_COMMAND")"' ERR + + + + +START_TIME=$(date +%s) + ############################################################################### # basic config ############################################################################### @@ -662,7 +704,7 @@ apply_spamhaus_blocklist() { mkdir -p "$(dirname "$POLICY_FILE")" if ! wget -q "$EXIT_POLICY_LOCATION" -O "$POLICY_FILE" 2>/dev/null; then - arror "failed to download exit policy, using minimal blocklist" + error "failed to download exit policy, using minimal blocklist" cat >"$POLICY_FILE" <> "$LOG_FILE" exit $status diff --git a/scripts/nym-node-setup/quic_bridge_deployment.sh b/scripts/nym-node-setup/quic_bridge_deployment.sh index db14127016..585261bbef 100644 --- a/scripts/nym-node-setup/quic_bridge_deployment.sh +++ b/scripts/nym-node-setup/quic_bridge_deployment.sh @@ -7,6 +7,7 @@ # RUN AS ROOT set -euo pipefail +set +o errtrace # Colors RED="\033[0;31m" @@ -17,25 +18,39 @@ BOLD="\033[1m" RESET="\033[0m" # Logging -LOG_FILE="/var/log/nym-bridge-helper.log" +LOG_FILE="/var/log/nym/quic_bridge_deployment.log" mkdir -p "$(dirname "$LOG_FILE")" + +# rotate log if >10MB BEFORE writing START header +if [[ -f "$LOG_FILE" && $(stat -c%s "$LOG_FILE") -gt 10485760 ]]; then + mv "$LOG_FILE" "${LOG_FILE}.1" +fi + touch "$LOG_FILE" chmod 640 "$LOG_FILE" + +echo "----- $(date '+%Y-%m-%d %H:%M:%S') START quic-bridge-manager -----" | tee -a "$LOG_FILE" echo -e "${CYAN}Logs are being saved locally to:${RESET} $LOG_FILE" echo -e "${CYAN}These logs never leave your machine.${RESET}" echo "" | tee -a "$LOG_FILE" -# safe logger +# safe logger function log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" } -# simple redirection that keeps function scope intact +# global redirection, strip ANSI before writing to log add_log_redirection() { - exec > >(tee -a "$LOG_FILE") 2>&1 + exec > >(sed -u 's/\x1b\[[0-9;]*m//g' | tee -a "$LOG_FILE") 2>&1 } add_log_redirection +# trap errors with full visibility +trap 'log "ERROR: exit=$? line=$LINENO cmd=\"$BASH_COMMAND\""' ERR + +START_TIME=$(date +%s) + + # Constants / Paths REQUIRED_CMDS=(ip jq curl openssl dpkg) BRIDGE_BIN="/usr/local/bin/nym-bridge" @@ -493,7 +508,7 @@ full_bridge_setup() { echo "Step 2/6: Installing bridge binary..." install_bridge_binary echo "[Bridge Install] $(date '+%F %T') $( $BRIDGE_BIN --version 2>/dev/null || echo 'nym-bridge (unknown)')" \ - >> /var/log/nym-bridge-version.log + >> /var/log/nym/nym-bridge-version.log press_enter "Press Enter to continue..." echo "" @@ -533,10 +548,6 @@ full_bridge_setup() { echo -e "${YELLOW}------------------------------------------${RESET}" echo -e "All done! You can safely close this session." echo -e "${YELLOW}------------------------------------------${RESET}" - echo "" - echo "Logs saved locally at: $LOG_FILE" - echo "Operation 'full_bridge_setup' completed." - echo "" hr echo -e "${CYAN}Next steps and verification:${RESET}" @@ -574,22 +585,26 @@ full_bridge_setup() { graceful_exit() { local exit_code=$? - echo "" - echo -e "${YELLOW}------------------------------------------${RESET}" + END_TIME=$(date +%s) + ELAPSED=$((END_TIME - START_TIME)) + + # Only print success message when there were NO errors if [[ $exit_code -eq 0 ]]; then - echo -e "${GREEN}Setup completed successfully. Exiting cleanly.${RESET}" - else - echo -e "${RED}Script exited with errors (code: $exit_code).${RESET}" - echo "Check the log at: $LOG_FILE" + echo "Operation '${COMMAND}' completed." fi - echo -e "${YELLOW}------------------------------------------${RESET}" - echo "" - exec >&- 2>&- + + # END footer always logged + echo "----- $(date '+%Y-%m-%d %H:%M:%S') END operation ${COMMAND} (status $exit_code, duration ${ELAPSED}s) -----" >> "$LOG_FILE" + exit $exit_code } -trap graceful_exit EXIT # Command menu +COMMAND="${1:-help}" +trap 'log "ERROR: exit=$? line=$LINENO cmd=$(printf "%q" "$BASH_COMMAND")"' ERR + +trap graceful_exit EXIT + case "${1:-}" in full_bridge_setup) full_bridge_setup ;; install_bridge_binary) install_bridge_binary ;; @@ -615,5 +630,3 @@ case "${1:-}" in ;; esac -echo "Operation '${1:-help}' completed." - From 68eae18b8b45beb935d9d21f0c2275c1c10a622e Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:21:03 +0100 Subject: [PATCH 137/180] fix coloring and trap --- .../nym-node-setup/quic_bridge_deployment.sh | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/scripts/nym-node-setup/quic_bridge_deployment.sh b/scripts/nym-node-setup/quic_bridge_deployment.sh index 585261bbef..a1fe29ff3a 100644 --- a/scripts/nym-node-setup/quic_bridge_deployment.sh +++ b/scripts/nym-node-setup/quic_bridge_deployment.sh @@ -41,13 +41,11 @@ log() { # global redirection, strip ANSI before writing to log add_log_redirection() { - exec > >(sed -u 's/\x1b\[[0-9;]*m//g' | tee -a "$LOG_FILE") 2>&1 + exec > >(tee >(sed -u 's/\x1b\[[0-9;]*m//g' >> "$LOG_FILE")) + exec 2> >(tee >(sed -u 's/\x1b\[[0-9;]*m//g' >> "$LOG_FILE") >&2) } add_log_redirection -# trap errors with full visibility -trap 'log "ERROR: exit=$? line=$LINENO cmd=\"$BASH_COMMAND\""' ERR - START_TIME=$(date +%s) @@ -82,23 +80,28 @@ if [[ "$(id -u)" -ne 0 ]]; then fi # UI helpers -hr() { echo -e "${YELLOW}----------------------------------------${RESET}"; } +hr() { echo -e "${YELLOW}----------------------------------------${RESET}" ; } title() { echo -e "\n${YELLOW}==========================================${RESET}\n${YELLOW} $1${RESET}\n${YELLOW}==========================================${RESET}\n"; } ok() { echo -e "${GREEN}$1${RESET}"; } warn() { echo -e "${YELLOW}$1${RESET}"; } err() { echo -e "${RED}$1${RESET}"; } info() { echo -e "${CYAN}$1${RESET}"; } -press_enter() { read -r -p "$1"; } +press_enter() { + echo -n "$1" > /dev/tty + read -r _ < /dev/tty +} + # Disable pauses and interactive prompts for noninteractive mode if [[ "${NONINTERACTIVE:-0}" == "1" ]]; then - # all pauses become no-ops - press_enter() { :; } - - # silence any "enter path:" prompts - echo_prompt() { :; } + press_enter() { :; } + echo_prompt() { :; } else - echo_prompt() { echo -n "$1"; } + press_enter() { + echo -n "$1" > /dev/tty + read -r _ < /dev/tty + } + echo_prompt() { echo -n "$1"; } fi # Helper: detect dpkg dependency failure for libc6>=2.34 From c13b4aa745719a5060e80cddd1940c3a55f322b6 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:25:59 +0100 Subject: [PATCH 138/180] fix coloring and trap --- scripts/nym-node-setup/network-tunnel-manager.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index af14f62912..c5f6bbbf86 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -61,12 +61,14 @@ log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" } -# simple redirection that keeps function scope intact +# global redirection, strip ANSI before writing to log add_log_redirection() { - exec > >(sed -u 's/\x1b\[[0-9;]*m//g' | tee -a "$LOG_FILE") 2>&1 + exec > >(tee >(sed -u 's/\x1b\[[0-9;]*m//g' >> "$LOG_FILE")) + exec 2> >(tee >(sed -u 's/\x1b\[[0-9;]*m//g' >> "$LOG_FILE") >&2) } add_log_redirection + trap 'log "ERROR: exit=$? line=$LINENO cmd=$(printf "%q" "$BASH_COMMAND")"' ERR @@ -333,7 +335,7 @@ remove_duplicate_rules() { rm -f "$tmp6" else - error "no ipv6 rules found for $interface to deduplicate" + ok "no ipv6 rules found for $interface to deduplicate" fi ok "duplicate rule scan completed for $interface" From f1be6ae788ced8f8e5ccd315cc64eb73c247f55d Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Mon, 24 Nov 2025 12:38:25 +0100 Subject: [PATCH 139/180] @ rename $cmd -> item in exit_policy_install_deps --- scripts/nym-node-setup/network-tunnel-manager.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index c5f6bbbf86..3a464dd9fa 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -516,10 +516,10 @@ add_port_rules() { exit_policy_install_deps() { install_iptables_persistent - for cmd in iptables ip6tables ip grep sed awk wget curl; do - if ! command -v "$cmd" >/dev/null 2>&1; then - info "installing dependency: $cmd" - apt-get install -y "$cmd" + for item in iptables ip6tables ip grep sed awk wget curl; do + if ! command -v "$item" >/dev/null 2>&1; then + info "installing dependency: $item" + apt-get install -y "$item" fi done } From 2d37c33a3d8c8244c9417a0e175debbc7492c56d Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:42:38 +0100 Subject: [PATCH 140/180] tweak docs commands --- .../operators/snippets/routing-conf.mdx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/documentation/docs/components/operators/snippets/routing-conf.mdx b/documentation/docs/components/operators/snippets/routing-conf.mdx index ebb33e5ba9..44036091f4 100644 --- a/documentation/docs/components/operators/snippets/routing-conf.mdx +++ b/documentation/docs/components/operators/snippets/routing-conf.mdx @@ -62,7 +62,7 @@ Networking configuration across different ISPs and various operation systems doe New nym-node full configuration, Existing nym-node full configuration, - Step-by-step or Pprtial configuration + Step-by-step or Partial configuration ]} defaultIndex={0}> This design is meant for operators setting up a new node on a fresh machine and it will result with a complete server readiness for routing as Entry Gateway and Exit Gateway in both Mixnet and WireGuard mode. @@ -80,9 +80,14 @@ chmod +x network-tunnel-manager.sh && \ - **If you setting up a new node and not upgrading an existing one, keep it running and [bond](/operators/nodes/nym-node/bonding.mdx) your node now! Then come back here and follow the rest of the configuration.** -###### 3. Execute complete network configuration: +###### 3. Run command for configuration: +- For nodes with WireGuard enabled: ```sh -./network-tunnel-manager.sh complete_networking_setup +./network-tunnel-manager.sh complete_networking_configuration +``` +- For nodes with WireGuard disabled: +```sh +./network-tunnel-manager.sh full_tunnel_setup ``` @@ -98,9 +103,14 @@ chmod +x network-tunnel-manager.sh && \ ./network-tunnel-manager.sh --help ``` -###### 2. Execute complete network configuration: +###### 2. Run command for configuration: +- For nodes with WireGuard enabled: ```sh -./network-tunnel-manager.sh complete_networking_setup +./network-tunnel-manager.sh complete_networking_configuration +``` +- For nodes with WireGuard disabled: +```sh +./network-tunnel-manager.sh full_tunnel_setup ``` From 26f4dd8f39001ba8f7e8a8b06e842aeaa21b6206 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Mon, 24 Nov 2025 13:41:03 +0100 Subject: [PATCH 141/180] add another test --- .../nym-node-setup/network-tunnel-manager.sh | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 3a464dd9fa..9b3318c836 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -912,10 +912,43 @@ check_nym_exit_chain() { return $errors } +check_iptables_default_policies() { + info "checking base iptables default policies (INPUT/FORWARD)" + + local issues=0 + local input_policy forward_policy + + input_policy=$(iptables -S INPUT 2>/dev/null | awk 'NR==1 && $1=="-P" {print $3}') + forward_policy=$(iptables -S FORWARD 2>/dev/null | awk 'NR==1 && $1=="-P" {print $3}') + + if [[ -z "${input_policy:-}" ]]; then + error "unable to read INPUT default policy (iptables -S INPUT failed?)" + issues=1 + elif [[ "${input_policy^^}" != "DROP" ]]; then + error "INPUT default policy is ${input_policy^^}; expected DROP so traffic is only allowed by explicit rules." + issues=1 + else + ok "INPUT default policy is DROP" + fi + + if [[ -z "${forward_policy:-}" ]]; then + error "unable to read FORWARD default policy (iptables -S FORWARD failed?)" + issues=1 + elif [[ "${forward_policy^^}" != "DROP" ]]; then + error "FORWARD default policy is ${forward_policy^^}; expected DROP to ensure traffic only flows via NYM-EXIT rules." + issues=1 + else + ok "FORWARD default policy is DROP" + fi + + return $issues +} + check_firewall_setup() { info "checking ipv4 firewall ordering…" local errors=0 + check_iptables_default_policies || errors=1 check_forward_chain || errors=1 check_nym_exit_chain || errors=1 From 42c051dfa3e6165759f62fbaa0ddcf2505c108bc Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Mon, 24 Nov 2025 13:44:28 +0100 Subject: [PATCH 142/180] add default output test --- scripts/nym-node-setup/network-tunnel-manager.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 9b3318c836..852079ac09 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -916,10 +916,11 @@ check_iptables_default_policies() { info "checking base iptables default policies (INPUT/FORWARD)" local issues=0 - local input_policy forward_policy + local input_policy forward_policy output_policy input_policy=$(iptables -S INPUT 2>/dev/null | awk 'NR==1 && $1=="-P" {print $3}') forward_policy=$(iptables -S FORWARD 2>/dev/null | awk 'NR==1 && $1=="-P" {print $3}') + output_policy=$(iptables -S OUTPUT 2>/dev/null | awk 'NR==1 && $1=="-P" {print $3}') if [[ -z "${input_policy:-}" ]]; then error "unable to read INPUT default policy (iptables -S INPUT failed?)" @@ -941,6 +942,16 @@ check_iptables_default_policies() { ok "FORWARD default policy is DROP" fi + if [[ -z "${output_policy:-}" ]]; then + error "unable to read OUTPUT default policy (iptables -S OUTPUT failed?)" + issues=1 + elif [[ "${output_policy^^}" != "ACCEPT" ]]; then + error "OUTPUT default policy is ${output_policy^^}; expected ACCEPT" + issues=1 + else + ok "OUTPUT default policy is ACCEPT" + fi + return $issues } From de0ae687efae364a0a41b46ae1666e352236bf47 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:24:04 +0100 Subject: [PATCH 143/180] docs: specify command desc --- .../docs/components/operators/snippets/routing-conf.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/documentation/docs/components/operators/snippets/routing-conf.mdx b/documentation/docs/components/operators/snippets/routing-conf.mdx index 44036091f4..f39379f7ab 100644 --- a/documentation/docs/components/operators/snippets/routing-conf.mdx +++ b/documentation/docs/components/operators/snippets/routing-conf.mdx @@ -81,11 +81,11 @@ chmod +x network-tunnel-manager.sh && \ - **If you setting up a new node and not upgrading an existing one, keep it running and [bond](/operators/nodes/nym-node/bonding.mdx) your node now! Then come back here and follow the rest of the configuration.** ###### 3. Run command for configuration: -- For nodes with WireGuard enabled: +- Nodes with **WireGuard enabled**: Configures interfaces (`nymtun0` and `nymwg`), IPv4 and IPv6 routing, WireGuard exit policy and does validation tests ```sh ./network-tunnel-manager.sh complete_networking_configuration ``` -- For nodes with WireGuard disabled: +- Nodes with **WireGuard disabled**: Does everything like the command above *without WireGuard exit policy* ```sh ./network-tunnel-manager.sh full_tunnel_setup ``` @@ -104,11 +104,11 @@ chmod +x network-tunnel-manager.sh && \ ``` ###### 2. Run command for configuration: -- For nodes with WireGuard enabled: +- Nodes with **WireGuard enabled**: Configures interfaces (`nymtun0` and `nymwg`), IPv4 and IPv6 routing, WireGuard exit policy and does validation tests ```sh ./network-tunnel-manager.sh complete_networking_configuration ``` -- For nodes with WireGuard disabled: +- Nodes with **WireGuard disabled**: Does everything like the command above *without WireGuard exit policy* ```sh ./network-tunnel-manager.sh full_tunnel_setup ``` From 00d0ae0b5bb3b7da3cb51b76a8f00e4bf05df4cf Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:35:00 +0100 Subject: [PATCH 144/180] docs: add noninteractive mode for quic setup --- .../snippets/quic-bridge-deployment-script-setup.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/documentation/docs/components/operators/snippets/quic-bridge-deployment-script-setup.mdx b/documentation/docs/components/operators/snippets/quic-bridge-deployment-script-setup.mdx index 592269d728..0fd62d033f 100644 --- a/documentation/docs/components/operators/snippets/quic-bridge-deployment-script-setup.mdx +++ b/documentation/docs/components/operators/snippets/quic-bridge-deployment-script-setup.mdx @@ -32,6 +32,10 @@ chmod +x quic_bridge_deployment.sh ```sh ./quic_bridge_deployment.sh full_bridge_setup ``` +- If you prefer a non-interactive mode, run the command with this variable (and skip next step): +```sh +NONINTERACTIVE=1 quick_bridge_deployment.sh full_bridge_setup +``` ###### 3. Follow the interactive prompts - Make sure you don't just press enter to insert default values if your setup is different, for example in case of path to the config file From 8c3a7977507dc5b6873e388ff7578f4ad65f9f6e Mon Sep 17 00:00:00 2001 From: RadekSabacky Date: Mon, 24 Nov 2025 15:11:01 +0100 Subject: [PATCH 145/180] @ fix perform_pings --- scripts/nym-node-setup/network-tunnel-manager.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 3a464dd9fa..82630438d1 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -397,10 +397,10 @@ check_ip_routing() { perform_pings() { info "performing ipv4 ping to google.com" - ping -c 4 google.com || error "ipv4 ping failed" + ping -4 -c 4 google.com || error "ipv4 ping failed" echo "---------------------------" info "performing ipv6 ping to google.com" - ping6 -c 4 google.com || error "ipv6 ping failed" + ping6 -6 -c 4 google.com || error "ipv6 ping failed" } joke_through_tunnel() { From a293d6da7d2407d68513c4affe3eb116efb264d4 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:20:45 +0100 Subject: [PATCH 146/180] full_tunnel_setup to nym_tunnel_setup --- .../components/operators/snippets/routing-conf.mdx | 4 ++-- scripts/nym-node-setup/network-tunnel-manager.sh | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/documentation/docs/components/operators/snippets/routing-conf.mdx b/documentation/docs/components/operators/snippets/routing-conf.mdx index f39379f7ab..31c7148454 100644 --- a/documentation/docs/components/operators/snippets/routing-conf.mdx +++ b/documentation/docs/components/operators/snippets/routing-conf.mdx @@ -87,7 +87,7 @@ chmod +x network-tunnel-manager.sh && \ ``` - Nodes with **WireGuard disabled**: Does everything like the command above *without WireGuard exit policy* ```sh -./network-tunnel-manager.sh full_tunnel_setup +./network-tunnel-manager.sh nym_tunnel_setup ``` @@ -110,7 +110,7 @@ chmod +x network-tunnel-manager.sh && \ ``` - Nodes with **WireGuard disabled**: Does everything like the command above *without WireGuard exit policy* ```sh -./network-tunnel-manager.sh full_tunnel_setup +./network-tunnel-manager.sh nym_tunnel_setup ``` diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 71aa676822..75ea0cf080 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -1140,7 +1140,7 @@ exit_policy_run_tests() { # part 5: high level workflows ############################################################################### -full_tunnel_setup() { +nym_tunnel_setup() { # this mirrors your previous chain of calls but inside one script info "running full tunnel setup for ${TUNNEL_INTERFACE} and ${WG_INTERFACE}" @@ -1182,7 +1182,7 @@ exit_policy_install() { complete_networking_configuration() { info "starting complete networking configuration: tunnels + exit policy" - full_tunnel_setup + nym_tunnel_setup exit_policy_install check_firewall_setup || error "firewall order checks reported problems, please review output" exit_policy_run_tests || error "exit policy tests reported problems, please review output" @@ -1198,8 +1198,8 @@ cmd="${1:-help}" log "COMMAND: $cmd ARGS: $*" case "$cmd" in - full_tunnel_setup) - full_tunnel_setup + nym_tunnel_setup) + nym_tunnel_setup status=$? ;; exit_policy_install) @@ -1298,9 +1298,8 @@ usage: $0 [args] high level workflows: complete_networking_configuration Install tunnel interfaces, setup networking, iptables, wg exit policy & tests - exit_policy_install Install and configure wireguard exit policy - full_tunnel_setup Install tunnel interfaces & setup networking - + nym_tunnel_setup Install tunnel interfaces & setup networking + exit_policy_install Install and configure wireguard exit policy tunnel and nat helpers: adjust_ip_forwarding Enable ipv4/ipv6 forwarding via sysctl.d apply_iptables_rules Apply nat/forward rules for ${TUNNEL_INTERFACE} From e0c74c5eb0c442e0fc3a000cf05f2543b2b76f70 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:33:51 +0100 Subject: [PATCH 147/180] formatting fix ... LFG --- scripts/nym-node-setup/network-tunnel-manager.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh index 75ea0cf080..ab941e89b5 100644 --- a/scripts/nym-node-setup/network-tunnel-manager.sh +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -1298,8 +1298,8 @@ usage: $0 [args] high level workflows: complete_networking_configuration Install tunnel interfaces, setup networking, iptables, wg exit policy & tests - nym_tunnel_setup Install tunnel interfaces & setup networking - exit_policy_install Install and configure wireguard exit policy + nym_tunnel_setup Install tunnel interfaces & setup networking + exit_policy_install Install and configure wireguard exit policy tunnel and nat helpers: adjust_ip_forwarding Enable ipv4/ipv6 forwarding via sysctl.d apply_iptables_rules Apply nat/forward rules for ${TUNNEL_INTERFACE} From 4b292ca1426c59b3124a0849a4b1c285b5196406 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:02:28 +0100 Subject: [PATCH 148/180] update nym-node-cli guide --- .../outputs/api-scraping-outputs/time-now.md | 2 +- .../node-api-check-query-help.md | 2 +- .../nym-node-cli-install-help.md | 37 ++++++++++++++ documentation/docs/pages/operators/tools.mdx | 48 ++++++++++++++++++- .../scripts/next-scripts/python-prebuild.sh | 10 ++++ .../nym-node-setup/network-tunnel-manager.sh | 0 6 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md mode change 100644 => 100755 scripts/nym-node-setup/network-tunnel-manager.sh diff --git a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md index bfd8731655..da40fc5444 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md @@ -1 +1 @@ -Thursday, November 20th 2025, 12:33:19 UTC +Tuesday, November 25th 2025, 08:49:55 UTC diff --git a/documentation/docs/components/outputs/command-outputs/node-api-check-query-help.md b/documentation/docs/components/outputs/command-outputs/node-api-check-query-help.md index 999f3d4a6f..bfa068ddb0 100644 --- a/documentation/docs/components/outputs/command-outputs/node-api-check-query-help.md +++ b/documentation/docs/components/outputs/command-outputs/node-api-check-query-help.md @@ -11,7 +11,7 @@ options: --no_routing_history Display node stats without routing history --no_verloc_metrics Display node stats without verloc metrics -m, --markdown Display results in markdown format - -o, --output [OUTPUT] + -o [OUTPUT], --output [OUTPUT] Save results to file (in current dir or supply with path without filename) ``` diff --git a/documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md b/documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md new file mode 100644 index 0000000000..70baf6f736 --- /dev/null +++ b/documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md @@ -0,0 +1,37 @@ +```sh +usage: nym-node-cli install [-h] [-V] [-d BRANCH] [-v] + [--mode {mixnode,entry-gateway,exit-gateway}] + [--wireguard-enabled {true,false}] + [--hostname HOSTNAME] [--location LOCATION] + [--email EMAIL] [--moniker MONIKER] + [--description DESCRIPTION] + [--public-ip PUBLIC_IP] + [--nym-node-binary NYM_NODE_BINARY] + [--uplink-dev UPLINK_DEV] [--env KEY=VALUE] + +options: + -h, --help show this help message and exit + -V, --version show program's version number and exit + -d BRANCH, --dev BRANCH + Define github branch (default: develop) + -v, --verbose Show full error tracebacks + --mode {mixnode,entry-gateway,exit-gateway} + Node mode: 'mixnode', 'entry-gateway', or 'exit- + gateway' + --wireguard-enabled {true,false} + WireGuard functionality switch: true / false + --hostname HOSTNAME Node domain / hostname + --location LOCATION Node location (country code or name) + --email EMAIL Contact email for the node operator + --moniker MONIKER Public moniker displayed in explorer & NymVPN app + --description DESCRIPTION + Short public description of the node + --public-ip PUBLIC_IP + External IPv4 address (autodetected if omitted) + --nym-node-binary NYM_NODE_BINARY + URL for nym-node binary (autodetected if omitted) + --uplink-dev UPLINK_DEV + Override uplink interface used for NAT/FORWARD (e.g., + 'eth0'; autodetected if omitted) + --env KEY=VALUE (Optional) Extra ENV VARS, e.g. --env CUSTOM_KEY=value +``` diff --git a/documentation/docs/pages/operators/tools.mdx b/documentation/docs/pages/operators/tools.mdx index 8a801e526a..c1b2eb6df8 100644 --- a/documentation/docs/pages/operators/tools.mdx +++ b/documentation/docs/pages/operators/tools.mdx @@ -5,7 +5,13 @@ import { RunTabs } from 'components/operators/nodes/node-run-command-tabs'; import { VarInfo } from 'components/variable-info.tsx'; import { AccordionTemplate } from 'components/accordion-template.tsx'; import { Steps } from 'nextra/components'; +import NymNodeCliInstallHelp from 'components/outputs/command-outputs/nym-node-cli-install-help.md'; +export const NymNodeCliCommand = () => ( +
+ Arguments and options: ./nym-node-cli.py install --help +
+); # Tools @@ -45,10 +51,50 @@ chmod +x ./nym-node-cli.py ``` ###### 3. Run the program + +There are two ways how to run the program. fully interactive or with provided arguments. The former has a shorter initial command but then operators must fill the values on the go, the latter requires more complex command, but then there is a minimum prompts during the process. + +1. Fully interactive mode - just run: ``` -./nym-node-cli.py +./nym-node-cli.py install ``` + +2. Use arguments - this command to see all options: +```sh +./nym-node-cli.py install --help +``` +}> + + +- An example can look like this: +```sh +# substitute with your real values: + ./nym-node-cli.py install + --hostname node-install.devrel.nymte.ch + --moniker MainnetGW-DE + --description "This node is installed with nym-node-cli v1.2.0" + --wireguard-enabled true + --location DE + --mode exit-gateway + --email kawa_hesinkar@example.ku +``` + ###### 4. Read and follow the prompts + +There will be a few needed confirmations and in caser of running the program without the arguments, you will be prompted for values neccessary to setup a `nym-node` and configure your server. + +**Read these prompts carefully!** + +###### 5. Setup finished + +Congratulation, your node is installed. + +This version of the program does not prompt an operator for the local node moniker (`--id `) therefore it asigns your node an automatic one `default-nym-node`. + +All configuration and data of your node can be found in this location: +```sh +ls $HOME/.nym/nym-nodes/default-nym-node/ +``` ## CMD Reward Tracker diff --git a/documentation/scripts/next-scripts/python-prebuild.sh b/documentation/scripts/next-scripts/python-prebuild.sh index a78bfbf11e..9083b2fd57 100755 --- a/documentation/scripts/next-scripts/python-prebuild.sh +++ b/documentation/scripts/next-scripts/python-prebuild.sh @@ -59,4 +59,14 @@ echo '```sh' > ../../documentation/docs/components/outputs/command-outputs/nym-a ./nym-api --help >> ../../documentation/docs/components/outputs/command-outputs/nym-api-help.md && echo '```' >> ../../documentation/docs/components/outputs/command-outputs/nym-api-help.md && +cd ../../scripts/nym-node-setup + +echo '```sh' > ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md && +python ./nym-node-cli.py install --help >> ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md && +echo '```' >> ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md && + +echo '```sh' > ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md && +python ./nym-node-cli.py install --help >> ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md && +echo '```' >> ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md && + echo "prebuild finished" diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh old mode 100644 new mode 100755 From 4b0fbc663ae6c86733e9d295af7decdff35a90c2 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:51:41 +0100 Subject: [PATCH 149/180] improve formatting --- documentation/docs/pages/operators/tools.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/pages/operators/tools.mdx b/documentation/docs/pages/operators/tools.mdx index c1b2eb6df8..e6e5138df8 100644 --- a/documentation/docs/pages/operators/tools.mdx +++ b/documentation/docs/pages/operators/tools.mdx @@ -55,7 +55,7 @@ chmod +x ./nym-node-cli.py There are two ways how to run the program. fully interactive or with provided arguments. The former has a shorter initial command but then operators must fill the values on the go, the latter requires more complex command, but then there is a minimum prompts during the process. 1. Fully interactive mode - just run: -``` +```sh ./nym-node-cli.py install ``` From fe01c922c0d1ab63d8e7b04d27a82640eda20b03 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:52:58 +0100 Subject: [PATCH 150/180] improve formatting --- documentation/docs/pages/operators/tools.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/pages/operators/tools.mdx b/documentation/docs/pages/operators/tools.mdx index e6e5138df8..145e873965 100644 --- a/documentation/docs/pages/operators/tools.mdx +++ b/documentation/docs/pages/operators/tools.mdx @@ -59,7 +59,7 @@ There are two ways how to run the program. fully interactive or with provided ar ./nym-node-cli.py install ``` -2. Use arguments - this command to see all options: +2. Use arguments - run this command to see all options: ```sh ./nym-node-cli.py install --help ``` From a69b473ba10b943a3b3aaa62424aa277f3c7493f Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:00:14 +0100 Subject: [PATCH 151/180] update explorer part --- documentation/docs/pages/operators/tools.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/documentation/docs/pages/operators/tools.mdx b/documentation/docs/pages/operators/tools.mdx index 145e873965..121f3e9f03 100644 --- a/documentation/docs/pages/operators/tools.mdx +++ b/documentation/docs/pages/operators/tools.mdx @@ -27,9 +27,11 @@ Nym Network stats can be humanly read on some of the explorers and dashboards. - **[SpectreDAO Explorer](https://explorer.nym.spectredao.net/):** By operators for operators - currently the most used Nym explorer -- **[Nymesis](https://nymesis.vercel.app/):** A slick dashboard by operator community +- **[Nym Node Status Observatory](https://harbourmaster.nymtech.net/):** An explorer with a list of Nym Nodes, their probe and per node data -- **[Nym Harbourmaster](https://harbourmaster.nymtech.net/):** A dashboard showing results of Gateway probes and more (*in development*) +- **[Nym Node Status UI](https://node-status.nym.com/):** A dashboard displaying results of Gateway probes and much more in one table + +- **[Nymesis](https://nymesis.vercel.app/):** A slick dashboard by operator community ## Nym Node CLI From 1b79107726992ab3e9f3d8cf552ea2671ea08634 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:02:26 +0100 Subject: [PATCH 152/180] update explorer part --- documentation/docs/pages/operators/tools.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/pages/operators/tools.mdx b/documentation/docs/pages/operators/tools.mdx index 121f3e9f03..dc047b3724 100644 --- a/documentation/docs/pages/operators/tools.mdx +++ b/documentation/docs/pages/operators/tools.mdx @@ -27,7 +27,7 @@ Nym Network stats can be humanly read on some of the explorers and dashboards. - **[SpectreDAO Explorer](https://explorer.nym.spectredao.net/):** By operators for operators - currently the most used Nym explorer -- **[Nym Node Status Observatory](https://harbourmaster.nymtech.net/):** An explorer with a list of Nym Nodes, their probe and per node data +- **[Nym Node Status Observatory](https://harbourmaster.nymtech.net/):** An explorer with a list of Nym Nodes, their properformance and per node data - **[Nym Node Status UI](https://node-status.nym.com/):** A dashboard displaying results of Gateway probes and much more in one table From 1aec4c2f8ed2e6e9aac91b555107c673e1c89fc7 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:11:16 +0100 Subject: [PATCH 153/180] fix typo --- documentation/docs/pages/operators/tools.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/pages/operators/tools.mdx b/documentation/docs/pages/operators/tools.mdx index dc047b3724..381d2febbd 100644 --- a/documentation/docs/pages/operators/tools.mdx +++ b/documentation/docs/pages/operators/tools.mdx @@ -83,7 +83,7 @@ There are two ways how to run the program. fully interactive or with provided ar ###### 4. Read and follow the prompts -There will be a few needed confirmations and in caser of running the program without the arguments, you will be prompted for values neccessary to setup a `nym-node` and configure your server. +There ware a few required confirmations and in case of running the program without the arguments, you will be prompted for values neccessary to setup a `nym-node` and configure your server. **Read these prompts carefully!** From f1024bc976f811f0bce5e6289d5e9c6e8e8e3898 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:13:46 +0100 Subject: [PATCH 154/180] improve formatting --- documentation/docs/pages/operators/tools.mdx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/documentation/docs/pages/operators/tools.mdx b/documentation/docs/pages/operators/tools.mdx index 381d2febbd..c29ecd6038 100644 --- a/documentation/docs/pages/operators/tools.mdx +++ b/documentation/docs/pages/operators/tools.mdx @@ -71,14 +71,14 @@ There are two ways how to run the program. fully interactive or with provided ar - An example can look like this: ```sh # substitute with your real values: - ./nym-node-cli.py install - --hostname node-install.devrel.nymte.ch - --moniker MainnetGW-DE - --description "This node is installed with nym-node-cli v1.2.0" - --wireguard-enabled true - --location DE - --mode exit-gateway - --email kawa_hesinkar@example.ku +./nym-node-cli.py install + --hostname node-install.devrel.nymte.ch + --moniker MainnetGW-DE + --description "This node is installed with nym-node-cli v1.2.0" + --wireguard-enabled true + --location DE + --mode exit-gateway + --email kawa_hesinkar@example.ku ``` ###### 4. Read and follow the prompts From 6eb8f29235093a43d4314534350ccaf7d878e808 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 25 Nov 2025 13:16:31 +0100 Subject: [PATCH 155/180] Statistics API v2 (#6227) * vpn client report v2 * report v2 support in nym-stats API * version bump * CI fix while we're at it * more CI fix * needed the dind after all * PR comments --- .../ci-check-nym-stats-api-version.yml | 2 +- .github/workflows/greetings.yml | 6 +- Cargo.lock | 3 +- common/statistics/Cargo.toml | 2 +- common/statistics/src/report/vpn_client.rs | 54 ++++++++- ...1c9df43603232e3be42b0573013cd74226518.json | 33 ++++++ ...3bea9553fb05a2f656cfff6598f12a21c99ba.json | 19 ---- nym-statistics-api/Cargo.toml | 5 +- .../migrations/003_sessions.sql | 29 +++++ nym-statistics-api/src/http/api/mod.rs | 2 +- nym-statistics-api/src/http/api/stats.rs | 79 ++++++++++--- nym-statistics-api/src/network_view.rs | 27 ++++- nym-statistics-api/src/storage/mod.rs | 90 +++++++++------ nym-statistics-api/src/storage/models.rs | 104 +++++++++++++----- 14 files changed, 340 insertions(+), 115 deletions(-) create mode 100644 nym-statistics-api/.sqlx/query-14d75cdd34201313e34ae7f0b931c9df43603232e3be42b0573013cd74226518.json delete mode 100644 nym-statistics-api/.sqlx/query-dce9f3dae7ae0dc5953d1f69e843bea9553fb05a2f656cfff6598f12a21c99ba.json create mode 100644 nym-statistics-api/migrations/003_sessions.sql diff --git a/.github/workflows/ci-check-nym-stats-api-version.yml b/.github/workflows/ci-check-nym-stats-api-version.yml index cfdddf2b25..130bb841d2 100644 --- a/.github/workflows/ci-check-nym-stats-api-version.yml +++ b/.github/workflows/ci-check-nym-stats-api-version.yml @@ -10,7 +10,7 @@ env: jobs: check-if-tag-exists: - runs-on: arc-ubuntu-22.04-dind + runs-on: arc-linux-latest-dind steps: - name: Checkout repo uses: actions/checkout@v4 diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml index de25d37594..a6e746c65b 100644 --- a/.github/workflows/greetings.yml +++ b/.github/workflows/greetings.yml @@ -8,6 +8,6 @@ jobs: steps: - uses: actions/first-interaction@v3 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-message: 'Thank you for raising this issue' - pr-message: 'Thank you for making this first PR' + repo_token: ${{ secrets.GITHUB_TOKEN }} + issue_message: 'Thank you for raising this issue' + pr_message: 'Thank you for making this first PR' diff --git a/Cargo.lock b/Cargo.lock index 7ec3531bb6..b7f0f8fe6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7197,7 +7197,7 @@ dependencies = [ [[package]] name = "nym-statistics-api" -version = "0.2.1" +version = "0.3.0" dependencies = [ "anyhow", "axum", @@ -7211,7 +7211,6 @@ dependencies = [ "nym-statistics-common", "nym-task", "nym-validator-client", - "serde", "serde_json", "sqlx", "time", diff --git a/common/statistics/Cargo.toml b/common/statistics/Cargo.toml index 18a8ca8fc9..b0f122646c 100644 --- a/common/statistics/Cargo.toml +++ b/common/statistics/Cargo.toml @@ -17,7 +17,7 @@ serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } thiserror = { workspace = true } -time = { workspace = true } +time = { workspace = true, features = ["serde"] } tokio = { workspace = true } si-scale = { workspace = true } strum = { workspace = true } diff --git a/common/statistics/src/report/vpn_client.rs b/common/statistics/src/report/vpn_client.rs index ab1260cae5..3a4cf5d8a8 100644 --- a/common/statistics/src/report/vpn_client.rs +++ b/common/statistics/src/report/vpn_client.rs @@ -2,9 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only use serde::{Deserialize, Serialize}; +use time::Date; -const KIND: &str = "vpn_client_stats_report"; -const VERSION: &str = "v1"; +const BASIC_REPORT_KIND: &str = "vpn_client_stats_report"; +const SESSION_REPORT_KIND: &str = "vpn_client_session_report"; +const VERSION_1: &str = "v1"; +const VERSION_2: &str = "v2"; #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[derive(Debug, Clone, Serialize, Deserialize)] @@ -13,15 +16,14 @@ pub struct VpnClientStatsReport { pub api_version: String, pub stats_id: String, pub static_information: StaticInformationReport, - //SW called it basic so we can swap it easily down the line for more data pub basic_usage: Option, } impl VpnClientStatsReport { pub fn new(stats_id: String, static_information: StaticInformationReport) -> Self { VpnClientStatsReport { - kind: KIND.into(), - api_version: VERSION.into(), + kind: BASIC_REPORT_KIND.into(), + api_version: VERSION_1.into(), stats_id, static_information, basic_usage: None, @@ -34,6 +36,34 @@ impl VpnClientStatsReport { self } } + +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VpnClientStatsReportV2 { + pub kind: String, + pub api_version: String, + pub stats_id: String, + + pub static_information: StaticInformationReport, + pub session_report: SessionReport, +} + +impl VpnClientStatsReportV2 { + pub fn new( + stats_id: String, + static_information: StaticInformationReport, + session_report: SessionReport, + ) -> Self { + VpnClientStatsReportV2 { + kind: SESSION_REPORT_KIND.into(), + api_version: VERSION_2.into(), + stats_id, + static_information, + session_report, + } + } +} + #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StaticInformationReport { @@ -49,3 +79,17 @@ pub struct UsageReport { pub connection_time_ms: Option, pub two_hop: bool, } + +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionReport { + pub start_day_utc: Date, + pub connection_time_ms: i32, + pub tunnel_type: String, + pub retry_attempt: i32, + pub session_duration_min: i32, + pub disconnection_time_ms: i32, + pub exit_id: String, + pub follow_up_id: Option, + pub error: Option, +} diff --git a/nym-statistics-api/.sqlx/query-14d75cdd34201313e34ae7f0b931c9df43603232e3be42b0573013cd74226518.json b/nym-statistics-api/.sqlx/query-14d75cdd34201313e34ae7f0b931c9df43603232e3be42b0573013cd74226518.json new file mode 100644 index 0000000000..a16fa1997d --- /dev/null +++ b/nym-statistics-api/.sqlx/query-14d75cdd34201313e34ae7f0b931c9df43603232e3be42b0573013cd74226518.json @@ -0,0 +1,33 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO report_v2(\n received_at,\n source_ip,\n from_mixnet,\n country_code,\n report_version,\n device_id,\n os_type,\n os_version,\n architecture,\n app_version,\n user_agent,\n start_day_utc,\n connection_time_ms,\n tunnel_type,\n retry_attempt,\n session_duration_min,\n disconnection_time_ms,\n exit_id,\n follow_up_id,\n error)\n VALUES ($1::timestamptz, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz", + "Text", + "Bool", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Date", + "Int4", + "Text", + "Int4", + "Int4", + "Int4", + "Text", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "14d75cdd34201313e34ae7f0b931c9df43603232e3be42b0573013cd74226518" +} diff --git a/nym-statistics-api/.sqlx/query-dce9f3dae7ae0dc5953d1f69e843bea9553fb05a2f656cfff6598f12a21c99ba.json b/nym-statistics-api/.sqlx/query-dce9f3dae7ae0dc5953d1f69e843bea9553fb05a2f656cfff6598f12a21c99ba.json deleted file mode 100644 index b477ea243d..0000000000 --- a/nym-statistics-api/.sqlx/query-dce9f3dae7ae0dc5953d1f69e843bea9553fb05a2f656cfff6598f12a21c99ba.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO connection_stats (\n received_at,\n connection_time_ms,\n two_hop,\n source_ip,\n country_code,\n from_mixnet) VALUES ($1::timestamptz, $2, $3, $4, $5, $6)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Timestamptz", - "Int4", - "Bool", - "Text", - "Text", - "Bool" - ] - }, - "nullable": [] - }, - "hash": "dce9f3dae7ae0dc5953d1f69e843bea9553fb05a2f656cfff6598f12a21c99ba" -} diff --git a/nym-statistics-api/Cargo.toml b/nym-statistics-api/Cargo.toml index e9e51ac626..769aeacb33 100644 --- a/nym-statistics-api/Cargo.toml +++ b/nym-statistics-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-statistics-api" -version = "0.2.1" +version = "0.3.0" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -20,14 +20,13 @@ axum-client-ip.workspace = true axum-extra = { workspace = true, features = ["typed-header"] } celes.workspace = true clap = { workspace = true, features = ["cargo", "derive", "env", "string"] } -serde = { workspace = true, features = ["derive"] } serde_json.workspace = true sqlx = { workspace = true, features = [ "runtime-tokio-rustls", "postgres", "time", ] } -time.workspace = true +time = { workspace = true, features = ["serde-human-readable"] } tokio = { workspace = true, features = ["rt-multi-thread"] } tokio-util.workspace = true tracing.workspace = true diff --git a/nym-statistics-api/migrations/003_sessions.sql b/nym-statistics-api/migrations/003_sessions.sql new file mode 100644 index 0000000000..ecd65d9173 --- /dev/null +++ b/nym-statistics-api/migrations/003_sessions.sql @@ -0,0 +1,29 @@ +CREATE TABLE report_v2 ( + -- some info about the report, inferred from when/from where we got it + received_at TIMESTAMP WITH TIME ZONE NOT NULL, + source_ip TEXT, + from_mixnet BOOLEAN, + country_code TEXT, + report_version TEXT, + + -- some infos about the device sending the report + device_id TEXT NOT NULL, + os_type TEXT, + os_version TEXT, + architecture TEXT, + app_version TEXT, + user_agent TEXT, + + -- session info + start_day_utc DATE, + connection_time_ms INTEGER, + tunnel_type TEXT, + retry_attempt INTEGER, + session_duration_min INTEGER, + disconnection_time_ms INTEGER, + exit_id TEXT, + follow_up_id TEXT, + error TEXT +); + +CREATE INDEX idx_report_v2_received_at ON report_v2 (received_at); diff --git a/nym-statistics-api/src/http/api/mod.rs b/nym-statistics-api/src/http/api/mod.rs index dd03791238..bbbe5351e2 100644 --- a/nym-statistics-api/src/http/api/mod.rs +++ b/nym-statistics-api/src/http/api/mod.rs @@ -24,7 +24,7 @@ impl RouterBuilder { ) .route( "/", - axum::routing::get(|| async { Redirect::permanent("/swagger") }), // SW let's redirect to a blogpost explaining the stats collection process once it exists + axum::routing::get(|| async { Redirect::permanent("/swagger") }), ) .nest("/v1", Router::new().nest("/stats", stats::routes())); diff --git a/nym-statistics-api/src/http/api/stats.rs b/nym-statistics-api/src/http/api/stats.rs index ab6b69926d..f085a03172 100644 --- a/nym-statistics-api/src/http/api/stats.rs +++ b/nym-statistics-api/src/http/api/stats.rs @@ -1,7 +1,7 @@ use axum::{Json, Router, extract::State}; use axum_client_ip::InsecureClientIp; use axum_extra::{TypedHeader, headers::UserAgent}; -use nym_statistics_common::report::vpn_client::VpnClientStatsReport; +use nym_statistics_common::report::vpn_client::{VpnClientStatsReport, VpnClientStatsReportV2}; use tracing::debug; use crate::{ @@ -9,11 +9,13 @@ use crate::{ error::{HttpError, HttpResult}, state::AppState, }, - storage::models::{ConnectionInfoDto, DailyActiveDeviceDto, StatsReportV1Dto}, + storage::models::{DailyActiveDeviceDto, StatsReportV1Dto, StatsReportV2Dto}, }; pub(crate) fn routes() -> Router { - Router::new().route("/report", axum::routing::post(submit_stats_report)) + Router::new() + .route("/report", axum::routing::post(submit_stats_report)) + .route("/session", axum::routing::post(submit_session_report)) } #[utoipa::path( @@ -44,19 +46,11 @@ async fn submit_stats_report( let maybe_location = gateway_record.unwrap_or_default(); if from_mixnet { - debug!("Received a report from the network"); + debug!("Received a V1 report from the network"); } else { - debug!("Received a report from outside of the network"); + debug!("Received a V1 report from outside of the network"); } - let active_device = DailyActiveDeviceDto::new(now, &report, user_agent.clone(), from_mixnet); - let maybe_connection_info = ConnectionInfoDto::maybe_new( - now, - &report, - insecure_ip_addr.0, - maybe_location, - from_mixnet, - ); let stats_report = StatsReportV1Dto::new( now, @@ -75,7 +69,64 @@ async fn submit_stats_report( state .storage() - .store_legacy_vpn_client_report(active_device, maybe_connection_info) + .store_active_device(active_device) + .await + .map_err(HttpError::internal_with_logging)?; + + Ok(Json(())) +} + +#[utoipa::path( + post, + request_body = VpnClientStatsReportV2, + tag = "Stats", + path = "/session", + context_path = "/v1/stats", + responses( + (status = 200) + ) +)] +#[tracing::instrument(level = "info", skip_all)] +async fn submit_session_report( + State(mut state): State, + TypedHeader(user_agent): TypedHeader, + insecure_ip_addr: InsecureClientIp, // This is the reverse proxy IP for now, but maybe in the future? + Json(report): Json, +) -> HttpResult> { + let now = time::OffsetDateTime::now_utc(); + let gateway_record = state + .network_view() + .get_country_by_id(&report.session_report.exit_id) + .await; + + let from_mixnet = gateway_record.is_some(); + let maybe_location = gateway_record.unwrap_or_default(); + + if from_mixnet { + debug!("Received a V2 report from the network"); + } else { + debug!("Received a V2 report from outside of the network"); + } + let active_device = DailyActiveDeviceDto::new_v2(now, &report, user_agent.clone(), from_mixnet); + + let stats_report = StatsReportV2Dto::new( + now, + &report, + user_agent, + from_mixnet, + insecure_ip_addr.0, + maybe_location, + ); + + state + .storage() + .store_active_device(active_device) + .await + .map_err(HttpError::internal_with_logging)?; + + state + .storage() + .store_vpn_client_report_v2(stats_report) .await .map_err(HttpError::internal_with_logging)?; diff --git a/nym-statistics-api/src/network_view.rs b/nym-statistics-api/src/network_view.rs index db3a153de0..1491a00903 100644 --- a/nym-statistics-api/src/network_view.rs +++ b/nym-statistics-api/src/network_view.rs @@ -20,6 +20,7 @@ use tracing::{error, info, trace, warn}; const NETWORK_CACHE_TTL: Duration = Duration::from_secs(600); type IpToCountryMap = HashMap>; +type IdToCountryMap = HashMap>; // SW this should use a proper NS API client once it exists struct NodesQuerier { @@ -45,19 +46,24 @@ impl NetworkView { fn new_empty() -> Self { NetworkView { inner: Arc::new(RwLock::new(NetworkViewInner { - network_nodes: HashMap::new(), + ip_to_country: HashMap::new(), + id_to_country: HashMap::new(), })), } } pub(crate) async fn get_country_by_ip(&self, ip_addr: &IpAddr) -> Option> { - self.inner.read().await.network_nodes.get(ip_addr).copied() + self.inner.read().await.ip_to_country.get(ip_addr).copied() + } + pub(crate) async fn get_country_by_id(&self, id_key: &str) -> Option> { + self.inner.read().await.id_to_country.get(id_key).copied() } } #[derive(Debug)] struct NetworkViewInner { - network_nodes: IpToCountryMap, + ip_to_country: IpToCountryMap, + id_to_country: IdToCountryMap, } pub struct NetworkRefresher { @@ -112,7 +118,7 @@ impl NetworkRefresher { let nodes = querier.current_nymnodes().await?; // collect all known/allowed nodes information - let known_nodes = nodes + let ip_to_country = nodes .iter() .flat_map(|n| { n.description @@ -124,8 +130,19 @@ impl NetworkRefresher { }) .collect::>(); + let id_to_country = nodes + .iter() + .map(|n| { + ( + n.ed25519_identity_key().to_base58_string(), + n.description.auxiliary_details.location, + ) + }) + .collect::>(); + let mut network_guard = self.network.inner.write().await; - network_guard.network_nodes = known_nodes; + network_guard.ip_to_country = ip_to_country; + network_guard.id_to_country = id_to_country; } Ok(()) diff --git a/nym-statistics-api/src/storage/mod.rs b/nym-statistics-api/src/storage/mod.rs index f6547da55c..3008855013 100644 --- a/nym-statistics-api/src/storage/mod.rs +++ b/nym-statistics-api/src/storage/mod.rs @@ -1,5 +1,4 @@ use anyhow::{Result, anyhow}; -use models::{ConnectionInfoDto, DailyActiveDeviceDto}; use sqlx::{ Executor, migrate::Migrator, @@ -7,7 +6,7 @@ use sqlx::{ }; use std::{path::PathBuf, str::FromStr}; -use crate::storage::models::StatsReportV1Dto; +use crate::storage::models::{DailyActiveDeviceDto, StatsReportV1Dto, StatsReportV2Dto}; pub(crate) mod models; @@ -62,19 +61,10 @@ impl StatisticsStorage { }) } - pub(crate) async fn store_legacy_vpn_client_report( - &mut self, + pub(crate) async fn store_active_device( + &self, active_device: DailyActiveDeviceDto, - connection_info: Option, ) -> Result<()> { - self.store_device(active_device).await?; - if let Some(connection_info) = connection_info { - self.store_connection_stats(connection_info).await?; - } - Ok(()) - } - - async fn store_device(&self, active_device: DailyActiveDeviceDto) -> Result<()> { sqlx::query!( r#"INSERT INTO active_device ( day, @@ -101,27 +91,6 @@ impl StatisticsStorage { Ok(()) } - async fn store_connection_stats(&self, connection_info: ConnectionInfoDto) -> Result<()> { - sqlx::query!( - r#"INSERT INTO connection_stats ( - received_at, - connection_time_ms, - two_hop, - source_ip, - country_code, - from_mixnet) VALUES ($1::timestamptz, $2, $3, $4, $5, $6)"#, - connection_info.received_at as time::OffsetDateTime, - connection_info.connection_time_ms, - connection_info.two_hop, - connection_info.received_from, - connection_info.country_code, - connection_info.from_mixnet - ) - .execute(&self.connection_pool) - .await?; - Ok(()) - } - pub(crate) async fn store_vpn_client_report( &mut self, report_v1: StatsReportV1Dto, @@ -158,4 +127,57 @@ impl StatisticsStorage { .await?; Ok(()) } + + pub(crate) async fn store_vpn_client_report_v2( + &self, + report_v2: StatsReportV2Dto, + ) -> Result<()> { + sqlx::query!( + r#"INSERT INTO report_v2( + received_at, + source_ip, + from_mixnet, + country_code, + report_version, + device_id, + os_type, + os_version, + architecture, + app_version, + user_agent, + start_day_utc, + connection_time_ms, + tunnel_type, + retry_attempt, + session_duration_min, + disconnection_time_ms, + exit_id, + follow_up_id, + error) + VALUES ($1::timestamptz, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)"#, + report_v2.received_at as time::OffsetDateTime, + report_v2.received_from, + report_v2.from_mixnet, + report_v2.country_code, + report_v2.report_version, + report_v2.stats_id, + report_v2.os_type, + report_v2.os_version, + report_v2.os_arch, + report_v2.app_version, + report_v2.user_agent, + report_v2.start_day_utc, + report_v2.connection_time_ms, + report_v2.tunnel_type, + report_v2.retry_attempt, + report_v2.session_duration_min, + report_v2.disconnection_time_ms, + report_v2.exit_id, + report_v2.follow_up_id, + report_v2.error + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } } diff --git a/nym-statistics-api/src/storage/models.rs b/nym-statistics-api/src/storage/models.rs index 95112ae64a..b7c5ba1b83 100644 --- a/nym-statistics-api/src/storage/models.rs +++ b/nym-statistics-api/src/storage/models.rs @@ -2,7 +2,7 @@ use std::net::IpAddr; use axum_extra::headers::UserAgent; use celes::Country; -use nym_statistics_common::report::vpn_client::VpnClientStatsReport; +use nym_statistics_common::report::vpn_client::{VpnClientStatsReport, VpnClientStatsReportV2}; use time::{Date, OffsetDateTime}; pub type StatsId = String; @@ -37,38 +37,25 @@ impl DailyActiveDeviceDto { from_mixnet, } } -} - -#[derive(Debug, Clone, sqlx::FromRow)] -pub(crate) struct ConnectionInfoDto { - pub(crate) received_at: OffsetDateTime, - pub(crate) received_from: String, - pub(crate) connection_time_ms: Option, - pub(crate) two_hop: bool, - pub(crate) country_code: Option, - pub(crate) from_mixnet: bool, -} - -impl ConnectionInfoDto { - pub(crate) fn maybe_new( + pub(crate) fn new_v2( received_at: OffsetDateTime, - stats_report: &VpnClientStatsReport, - received_from: IpAddr, - maybe_country: Option, + stats_report: &VpnClientStatsReportV2, + user_agent: UserAgent, from_mixnet: bool, - ) -> Option { - stats_report.basic_usage.as_ref().map(|usage_report| Self { - received_at, - received_from: received_from.to_string(), - connection_time_ms: usage_report.connection_time_ms, - two_hop: usage_report.two_hop, - country_code: maybe_country.map(|c| c.alpha2.into()), + ) -> Self { + Self { + day: received_at.date(), + stats_id: stats_report.stats_id.clone(), + os_type: stats_report.static_information.os_type.clone(), + os_version: stats_report.static_information.os_version.clone(), + os_arch: stats_report.static_information.os_arch.clone(), + app_version: stats_report.static_information.app_version.clone(), + user_agent: user_agent.to_string(), from_mixnet, - }) + } } } -// New structure. The two above will be removed when it is confirmed to work #[derive(Debug, Clone, sqlx::FromRow)] pub(crate) struct StatsReportV1Dto { pub(crate) received_at: OffsetDateTime, @@ -116,3 +103,66 @@ impl StatsReportV1Dto { report } } + +#[derive(Debug, Clone, sqlx::FromRow)] +pub(crate) struct StatsReportV2Dto { + // Report metadata + pub(crate) received_at: OffsetDateTime, + pub(crate) received_from: String, + pub(crate) from_mixnet: bool, + pub(crate) country_code: Option, + pub(crate) report_version: String, + + // Device info + pub(crate) stats_id: StatsId, + pub(crate) os_type: String, + pub(crate) os_version: Option, + pub(crate) os_arch: String, + pub(crate) app_version: String, + pub(crate) user_agent: String, + + // session info + pub(crate) start_day_utc: Date, + pub(crate) connection_time_ms: i32, + pub(crate) tunnel_type: String, + pub(crate) retry_attempt: i32, + pub(crate) session_duration_min: i32, + pub(crate) disconnection_time_ms: i32, + pub(crate) exit_id: String, + pub(crate) follow_up_id: Option, + pub(crate) error: Option, +} + +impl StatsReportV2Dto { + pub(crate) fn new( + received_at: OffsetDateTime, + stats_report: &VpnClientStatsReportV2, + user_agent: UserAgent, + from_mixnet: bool, + received_from: IpAddr, + maybe_country: Option, + ) -> Self { + Self { + received_at, + received_from: received_from.to_string(), + from_mixnet, + country_code: maybe_country.map(|c| c.alpha2.into()), + report_version: stats_report.api_version.clone(), + stats_id: stats_report.stats_id.clone(), + os_type: stats_report.static_information.os_type.clone(), + os_version: stats_report.static_information.os_version.clone(), + os_arch: stats_report.static_information.os_arch.clone(), + app_version: stats_report.static_information.app_version.clone(), + user_agent: user_agent.to_string(), + start_day_utc: stats_report.session_report.start_day_utc, + connection_time_ms: stats_report.session_report.connection_time_ms, + tunnel_type: stats_report.session_report.tunnel_type.clone(), + retry_attempt: stats_report.session_report.retry_attempt, + session_duration_min: stats_report.session_report.session_duration_min, + disconnection_time_ms: stats_report.session_report.disconnection_time_ms, + exit_id: stats_report.session_report.exit_id.clone(), + follow_up_id: stats_report.session_report.follow_up_id.clone(), + error: stats_report.session_report.error.clone(), + } + } +} From 6a96d8205b7fdc3b745667a9d67e9b26b218cbe8 Mon Sep 17 00:00:00 2001 From: benedetta davico <46782255+benedettadavico@users.noreply.github.com> Date: Wed, 26 Nov 2025 09:33:17 +0100 Subject: [PATCH 156/180] Bump version from 4.0.11-testing to 4.0.12 --- nym-node-status-api/nym-node-status-api/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nym-node-status-api/nym-node-status-api/Cargo.toml b/nym-node-status-api/nym-node-status-api/Cargo.toml index b32100a6ba..df6d5c12cf 100644 --- a/nym-node-status-api/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/nym-node-status-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-api" -version = "4.0.11-testing" +version = "4.0.12" authors.workspace = true repository.workspace = true homepage.workspace = true From 06717037e52175c555c5049e510325053531550a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:18:41 +0000 Subject: [PATCH 157/180] Bump js-yaml in /sdk/typescript/codegen/contract-clients (#6231) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.14.1 to 3.14.2. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 3.14.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../contract-clients/package-lock.json | 5020 +++++++---------- 1 file changed, 2011 insertions(+), 3009 deletions(-) diff --git a/sdk/typescript/codegen/contract-clients/package-lock.json b/sdk/typescript/codegen/contract-clients/package-lock.json index 219b1f89f7..37c65830b5 100644 --- a/sdk/typescript/codegen/contract-clients/package-lock.json +++ b/sdk/typescript/codegen/contract-clients/package-lock.json @@ -1,15 +1,15 @@ { "name": "@nymproject/contract-clients", - "version": "1.0.0", + "version": "1.3.1-rc0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@nymproject/contract-clients", - "version": "1.0.0", + "version": "1.3.1-rc0", "license": "Apache-2.0", "devDependencies": { - "@cosmwasm/ts-codegen": "^0.35.3", + "@cosmwasm/ts-codegen": "^1.13.3", "nodemon": "3.0.1", "npm-run-all": "^4.1.5", "reload": "^3.2.1", @@ -17,640 +17,175 @@ "typescript": "^4.6.2" } }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz", - "integrity": "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.22.10", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", - "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz", - "integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.18.10", - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-module-transforms": "^7.18.9", - "@babel/helpers": "^7.18.9", - "@babel/parser": "^7.18.10", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.18.10", - "@babel/types": "^7.18.10", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, "node_modules/@babel/generator": { - "version": "7.18.12", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.12.tgz", - "integrity": "sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", + "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.18.10", - "@jridgewell/gen-mapping": "^0.3.2", + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.10.tgz", - "integrity": "sha512-Av0qubwDQxC56DoUReVDeLfMEjYYSN1nZrTUrWkXd7hpU73ymRANkbuDm3yni9npkn+RXy9nNbEJZEzXr7xrfQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.10" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz", - "integrity": "sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.5", - "browserslist": "^4.21.9", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.10.tgz", - "integrity": "sha512-5IBb77txKYQPpOEdUdIhBx8VrZyDCQ+H82H0+5dX1TmuscP5vJKEE3cKurjtIw/vFwzbVH48VweE78kVDBrqjA==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz", - "integrity": "sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "regexpu-core": "^5.3.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", - "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", - "dev": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0-0" - } - }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-environment-visitor/node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", - "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", - "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", - "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz", - "integrity": "sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-wrap-function": "^7.22.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", - "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", - "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.10.tgz", - "integrity": "sha512-OnMhjWjuGYtdoO3FmsEFWvBStBAe2QOgwOLsLNDjN+aaiMD8InJk1/O3HSD8lkqTjCgg5YI34Tz15KNNA3p+nQ==", - "dev": true, - "dependencies": { - "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.10" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.10.tgz", - "integrity": "sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.10", - "@babel/types": "^7.22.10" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers/node_modules/@babel/generator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.10.tgz", - "integrity": "sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.10", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers/node_modules/@babel/parser": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.10.tgz", - "integrity": "sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/helpers/node_modules/@babel/traverse": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.10.tgz", - "integrity": "sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.10", - "@babel/generator": "^7.22.10", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.10", - "@babel/types": "^7.22.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz", - "integrity": "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.18.11", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz", - "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -658,1299 +193,75 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz", - "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", + "node_modules/@babel/parser/node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", - "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", - "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-static-block": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", - "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-export-default-from": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.18.10.tgz", - "integrity": "sha512-5H2N3R2aQFxkV4PIBUR/i7PUSwgTZjouJKzI8eKswfIjT0PhvzkPn0t0wIS5zn6maQuvtT0t1oHtMUz61LOuow==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-default-from": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-json-strings": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", - "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", - "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.18.9.tgz", - "integrity": "sha512-kDDHQ5rflIeY5xl69CEqGEZ0KY369ehsCIEbTGb4siHG5BE9sga/T0r0OUwyZNLMmZE79E1kbsqAjwFCW4ds6Q==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.18.8", - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.18.8" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", - "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", - "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-default-from": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.22.5.tgz", - "integrity": "sha512-ODAqWWXB/yReh/jVQDag/3/tl6lgBueQkk/TcfW/59Oykm4c8a55XloX0CTk2k2VJiFWMgHby9xNX29IbCv9dQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", - "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", - "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", - "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", - "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", - "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", - "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.10.tgz", - "integrity": "sha512-1+kVpGAOOI1Albt6Vse7c8pHzcZQdQKW+wJH+g8mCaszOdDVwRXa/slHPqIw+oJAJANTKDMuM2cBdV0Dg618Vg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz", - "integrity": "sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", - "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.10.tgz", - "integrity": "sha512-dPJrL0VOyxqLM9sritNbMSGx/teueHF/htMKrPT7DNxccXxRDPYqlgPFFdr8u+F+qUZOkZoXue/6rL5O5GduEw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", - "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", - "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", - "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", - "dev": true, - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", - "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", - "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", - "dev": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", - "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", - "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", - "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", - "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz", - "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==", - "dev": true, - "dependencies": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", - "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", - "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", - "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.10.tgz", - "integrity": "sha512-MMkQqZAZ+MGj+jGTG3OTuhKeBpNcO+0oCEbrGNEaOmiEn+1MzRyQlYsruGiU8RTK3zV6XwrVJTmwiDOyYK6J9g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", - "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", - "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", - "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "regenerator-transform": "^0.15.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", - "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.18.10.tgz", - "integrity": "sha512-q5mMeYAdfEbpBAgzl7tBre/la3LeCxmDO1+wMXRdPWbcoMjR3GiXlCLk7JBZVVye0bqTGNMbt0yYVXX1B1jEWQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.9", - "babel-plugin-polyfill-corejs2": "^0.3.2", - "babel-plugin-polyfill-corejs3": "^0.5.3", - "babel-plugin-polyfill-regenerator": "^0.4.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", - "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", - "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", - "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", - "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", - "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.10.tgz", - "integrity": "sha512-7++c8I/ymsDo4QQBAgbraXLzIM6jmfao11KgIBEYZRReWzNWH9NtNgJcyrZiXsOPh523FQm6LfpLyy/U5fn46A==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.10", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-typescript": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", - "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", - "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.18.10.tgz", - "integrity": "sha512-wVxs1yjFdW3Z/XkNfXKoblxoHgbtUF7/l3PvvP4m02Qz9TZ6uZGxRVYjSQeR87oQmHco9zWitW5J82DJ7sCjvA==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.18.8", - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-async-generator-functions": "^7.18.10", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-class-static-block": "^7.18.6", - "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.18.9", - "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.18.6", - "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.18.6", - "@babel/plugin-transform-async-to-generator": "^7.18.6", - "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.18.9", - "@babel/plugin-transform-classes": "^7.18.9", - "@babel/plugin-transform-computed-properties": "^7.18.9", - "@babel/plugin-transform-destructuring": "^7.18.9", - "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.9", - "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.18.8", - "@babel/plugin-transform-function-name": "^7.18.9", - "@babel/plugin-transform-literals": "^7.18.9", - "@babel/plugin-transform-member-expression-literals": "^7.18.6", - "@babel/plugin-transform-modules-amd": "^7.18.6", - "@babel/plugin-transform-modules-commonjs": "^7.18.6", - "@babel/plugin-transform-modules-systemjs": "^7.18.9", - "@babel/plugin-transform-modules-umd": "^7.18.6", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.18.6", - "@babel/plugin-transform-new-target": "^7.18.6", - "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.18.8", - "@babel/plugin-transform-property-literals": "^7.18.6", - "@babel/plugin-transform-regenerator": "^7.18.6", - "@babel/plugin-transform-reserved-words": "^7.18.6", - "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.18.9", - "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.10", - "@babel/plugin-transform-unicode-regex": "^7.18.6", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.18.10", - "babel-plugin-polyfill-corejs2": "^0.3.2", - "babel-plugin-polyfill-corejs3": "^0.5.3", - "babel-plugin-polyfill-regenerator": "^0.4.0", - "core-js-compat": "^3.22.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz", - "integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.22.5.tgz", - "integrity": "sha512-YbPaal9LxztSGhmndR46FmAbkJ/1fAsw293tSU+I5E5h+cnJ3d4GTwyUgGYmOXJYdGA+uNePle4qbaRzj2NISQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.5", - "@babel/plugin-transform-typescript": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", - "dev": true - }, "node_modules/@babel/runtime": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", - "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "dev": true, - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/template/node_modules/@babel/parser": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.10.tgz", - "integrity": "sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/template/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.18.11", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.11.tgz", - "integrity": "sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", + "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.18.10", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.18.11", - "@babel/types": "^7.18.10", - "debug": "^4.1.0", + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.1", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -1958,320 +269,848 @@ } }, "node_modules/@babel/types": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz", - "integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.18.10", - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@cosmwasm/ts-codegen": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@cosmwasm/ts-codegen/-/ts-codegen-0.35.3.tgz", - "integrity": "sha512-L7T4yjAQIWJ/h0YmqVjihzv78ZbqGD/f0G8EJTwccsPXqWzbcnXzQKsvtFXpDp6AzaJKVMchFHfp6ZPRP3boUQ==", + "node_modules/@chain-registry/types": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@chain-registry/types/-/types-0.17.1.tgz", + "integrity": "sha512-O0CgrtJgIlqXvZm1CqDZe/7jZz068O/uuCIoyDXCegFHK03rdHacKcDGwEIUuI0MNUf8YV3jdE4xHQMSAX+79w==", "dev": true, + "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@babel/core": "7.18.10", - "@babel/generator": "7.18.12", - "@babel/parser": "7.18.11", - "@babel/plugin-proposal-class-properties": "7.18.6", - "@babel/plugin-proposal-export-default-from": "7.18.10", - "@babel/plugin-proposal-object-rest-spread": "7.18.9", - "@babel/plugin-transform-runtime": "7.18.10", - "@babel/preset-env": "7.18.10", - "@babel/preset-typescript": "^7.18.6", - "@babel/runtime": "^7.18.9", - "@babel/traverse": "7.18.11", - "@babel/types": "7.18.10", + "@babel/runtime": "^7.21.0" + } + }, + "node_modules/@chain-registry/utils": { + "version": "1.51.261", + "resolved": "https://registry.npmjs.org/@chain-registry/utils/-/utils-1.51.261.tgz", + "integrity": "sha512-spdGKOB4ZdUBcPiFAMIXCwCi8Q5/dAXyRPzLXBCCm0DxBnhSfg6nx1iTin2ViXog1TOMykSthxi0dcdCANvCZA==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@chain-registry/types": "^0.50.261", + "bignumber.js": "9.1.2", + "sha.js": "^2.4.11" + } + }, + "node_modules/@chain-registry/utils/node_modules/@chain-registry/types": { + "version": "0.50.261", + "resolved": "https://registry.npmjs.org/@chain-registry/types/-/types-0.50.261.tgz", + "integrity": "sha512-+AEKPWMFdPfu9pBGn/BjjyJkuPB/7AzkvzCVFxGocn/HZsXVVvxvK+q6YwgsDzNAcAtq8GU39S0dNi/Obj/o/g==", + "dev": true, + "license": "SEE LICENSE IN LICENSE" + }, + "node_modules/@chain-registry/v2": { + "version": "1.71.237", + "resolved": "https://registry.npmjs.org/@chain-registry/v2/-/v2-1.71.237.tgz", + "integrity": "sha512-c10uXxFz1+51VR0szeB1phn482O03uJiSQ+mZXpeq7IBtU2Hst6Vj1Ttjbx+nlxekhoSGG1jZ0JQ1ewXWYQ6IA==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "peer": true, + "dependencies": { + "@chain-registry/v2-types": "^0.53.146" + } + }, + "node_modules/@chain-registry/v2-types": { + "version": "0.53.146", + "resolved": "https://registry.npmjs.org/@chain-registry/v2-types/-/v2-types-0.53.146.tgz", + "integrity": "sha512-GnqdNnEJ+ZTAc/0S9fQbwL5GpIvF3LZ/P3o6qh6EyITSiRA1Y55FXs59eSddltpPX5EgyWDwNpJjY+Q5uYlAkA==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "peer": true + }, + "node_modules/@cosmwasm/ts-codegen": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@cosmwasm/ts-codegen/-/ts-codegen-1.13.3.tgz", + "integrity": "sha512-BD+yJQZjQ6CK6+vuMpAVQKRK24OSxSCATRgpzo5v2NHoXpcALp6HUc6NU0u8hZjULJjjT5uChiSjNuucS1P+YA==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@babel/generator": "7.24.4", + "@babel/traverse": "7.24.1", + "@babel/types": "7.24.0", + "@chain-registry/types": "^0.17.1", + "@chain-registry/utils": "^1.51.71", + "@chain-registry/v2": "^1.71.229", + "@cosmwasm/ts-codegen-ast": "^1.9.3", + "@cosmwasm/ts-codegen-types": "^1.4.3", + "@interchainjs/amino": "1.17.1", + "@interchainjs/cosmos": "1.17.1", + "@interchainjs/types": "1.17.1", "@pyramation/json-schema-to-typescript": " 11.0.4", + "@types/rimraf": "3.0.2", + "@types/shelljs": "0.8.15", "case": "1.6.3", "dargs": "7.0.0", "deepmerge": "4.2.2", - "dotty": "0.1.2", "fuzzy": "0.1.3", - "glob": "8.0.3", + "glob": "^10", "inquirerer": "0.1.3", - "long": "^5.2.0", + "interchainjs": "1.17.1", + "minimatch": "^10.0.3", "minimist": "1.2.6", "mkdirp": "1.0.4", + "nested-obj": "0.0.1", "parse-package-name": "1.0.0", "rimraf": "3.0.2", - "shelljs": "0.8.5", - "wasm-ast-types": "^0.26.2" + "shelljs": "0.8.5" }, "bin": { - "cosmwasm-ts-codegen": "main/ts-codegen.js" + "cosmwasm-ts-codegen": "ts-codegen.js", + "ts-codegen": "ts-codegen.js" } }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "node_modules/@cosmwasm/ts-codegen-ast": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@cosmwasm/ts-codegen-ast/-/ts-codegen-ast-1.9.3.tgz", + "integrity": "sha512-ITtwVRbYr4fmjxq3xOc7wKsAK5xaIaLp7bEduGax2cgv54NL8i5/lv0iP84DUI11EEA9bDOWl6QtvwKBd8I12g==", "dev": true, + "license": "SEE LICENSE IN LICENSE", "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "@babel/types": "7.24.0", + "@cosmwasm/ts-codegen-types": "^1.4.3", + "ast-stringify": "0.2.1", + "case": "1.6.3", + "deepmerge": "4.2.2", + "minimatch": "^10.0.3" + } + }, + "node_modules/@cosmwasm/ts-codegen-types": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@cosmwasm/ts-codegen-types/-/ts-codegen-types-1.4.3.tgz", + "integrity": "sha512-WjNOpg8phEbzCfwo7cB2Iey/ipwKsSqazhx9k2ocTwSG+87zf1G1XGhU0K0FP4XTMvUvNjd8ovtCi4I1+kSMTA==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "minimatch": "^10.0.3" + } + }, + "node_modules/@ethersproject/abstract-provider": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz", + "integrity": "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-signer": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz", + "integrity": "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/address": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.8.0.tgz", + "integrity": "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/rlp": "^5.8.0" + } + }, + "node_modules/@ethersproject/base64": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.8.0.tgz", + "integrity": "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0" + } + }, + "node_modules/@ethersproject/bignumber": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.8.0.tgz", + "integrity": "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "bn.js": "^5.2.1" + } + }, + "node_modules/@ethersproject/bytes": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.8.0.tgz", + "integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/constants": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.8.0.tgz", + "integrity": "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0" + } + }, + "node_modules/@ethersproject/hash": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.8.0.tgz", + "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/keccak256": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", + "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/logger": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz", + "integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT" + }, + "node_modules/@ethersproject/networks": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.8.0.tgz", + "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/properties": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.8.0.tgz", + "integrity": "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/rlp": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.8.0.tgz", + "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/signing-key": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.8.0.tgz", + "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "bn.js": "^5.2.1", + "elliptic": "6.6.1", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/strings": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz", + "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/transactions": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.8.0.tgz", + "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0" + } + }, + "node_modules/@ethersproject/web": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.8.0.tgz", + "integrity": "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@interchainjs/amino": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/amino/-/amino-1.17.1.tgz", + "integrity": "sha512-MYEye+V4O7tITc/YofgGbiLT0kdXuK0BvuIYMFfq5+Tybq9tgFu/PYQ6CL3nyD6+S5H/rbgz9sUH31sPj7XaZg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@interchainjs/crypto": "1.17.1", + "@interchainjs/encoding": "1.17.1", + "@interchainjs/math": "1.17.1", + "@interchainjs/utils": "1.17.1" + } + }, + "node_modules/@interchainjs/auth": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/auth/-/auth-1.17.1.tgz", + "integrity": "sha512-4khXhRti537vqsSzSPfCwtarFBNvXkc8ISYFvqY43I2j0m0FbzZhNqjrXQn+xnihnjsF9ec/SqckVKdXX2DHFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@interchainjs/crypto": "1.17.1", + "@interchainjs/types": "1.17.1", + "@interchainjs/utils": "1.17.1", + "@noble/curves": "^1.1.0", + "@noble/hashes": "^1.3.1", + "@scure/bip32": "^1.0.10", + "bech32": "^2.0.0", + "elliptic": "^6.5.4", + "libsodium-wrappers-sumo": "^0.7.11" + } + }, + "node_modules/@interchainjs/auth/node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@interchainjs/cosmos": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/cosmos/-/cosmos-1.17.1.tgz", + "integrity": "sha512-Wzjv5VxaiZUD+sY2ErMv6QCTdrmV+Mus1VFHrMZo3T4gex6Z8fOOV1IaesN5+w44XM4YkF8CX+zSCerfqV6Dvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@interchainjs/auth": "1.17.1", + "@interchainjs/cosmos-types": "1.17.1", + "@interchainjs/crypto": "1.17.1", + "@interchainjs/encoding": "1.17.1", + "@interchainjs/types": "1.17.1", + "@interchainjs/utils": "1.17.1", + "@noble/curves": "^1.1.0", + "@noble/hashes": "^1.3.1", + "bech32": "^1.1.4", + "bignumber.js": "^9.1.0", + "bip39": "^3.1.0", + "decimal.js": "^10.4.3", + "deepmerge": "4.2.2", + "ws": "^8.14.0" + } + }, + "node_modules/@interchainjs/cosmos-types": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/cosmos-types/-/cosmos-types-1.17.1.tgz", + "integrity": "sha512-GU3dfQ2IHTVvClgctjY0RAdErwcG6fQskBh4Iqm6BDpcgpto2+nzES/DoLslx8ds9vBCujjx5zGFwr90fgScHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@interchainjs/math": "1.17.1", + "@interchainjs/types": "1.17.1", + "@interchainjs/utils": "1.17.1" + } + }, + "node_modules/@interchainjs/cosmos/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@interchainjs/crypto": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/crypto/-/crypto-1.17.1.tgz", + "integrity": "sha512-vrKtsRq4YX6pUukD73CbLLOD6tgwHb05GMOVhK0S8lGULiSoW8f76LvhOUgX13v5MSWiBzdSiAaVsAfTvkJePw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@interchainjs/encoding": "1.17.1", + "@interchainjs/math": "1.17.1", + "@interchainjs/utils": "1.17.1", + "@noble/hashes": "^1", + "bn.js": "^5.2.0", + "elliptic": "^6.5.4", + "libsodium-wrappers-sumo": "^0.7.11" + } + }, + "node_modules/@interchainjs/encoding": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/encoding/-/encoding-1.17.1.tgz", + "integrity": "sha512-ALRsvzEtSAHBiBw2z+i9bp9ICGpMN8L6cqS0AIpx+B4mFnuFp0TYi4m7CerklcDeV78ld+G6Xf5PbbQUGI0/hA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@interchainjs/math": "1.17.1", + "base64-js": "^1.3.0", + "bech32": "^1.1.4", + "readonly-date": "^1.0.0" + } + }, + "node_modules/@interchainjs/ethereum": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/ethereum/-/ethereum-1.17.1.tgz", + "integrity": "sha512-EOjB3Q8OusoCk5/zg0PlWJUoepKEOjm18DNEeAxUPmG5wDw8CIAp7wkUHF6lfGkE7ZFedwy74Uinc1eZZSDh1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@interchainjs/auth": "1.17.1", + "@interchainjs/types": "1.17.1", + "@interchainjs/utils": "1.17.1", + "@noble/curves": "^1.1.0", + "@noble/hashes": "^1.3.1", + "bip39": "^3.1.0", + "deepmerge": "4.2.2", + "ethereum-cryptography": "^3.1.0", + "rlp": "^3.0.0" + } + }, + "node_modules/@interchainjs/math": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/math/-/math-1.17.1.tgz", + "integrity": "sha512-4eOj6m95Iy3iNuhqf9F9iLBlV1U2G5L6CqjpROvn9fiO3H8DInXermZFP1CjFXley1Nrd8QfbV7OSkyfa0u8tA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.0" + } + }, + "node_modules/@interchainjs/pubkey": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/pubkey/-/pubkey-1.17.1.tgz", + "integrity": "sha512-hdfXFsZSVSg6tPzgd3Htw40dSY/nXTVJLrbZt6cKe6BGRiuPSQmOkka6AZn0Ze5Y4ULYfUMxpipFQc0ukfsd1A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@interchainjs/amino": "1.17.1", + "@interchainjs/cosmos-types": "1.17.1", + "@interchainjs/encoding": "1.17.1", + "@interchainjs/math": "1.17.1", + "@interchainjs/types": "1.17.1" + } + }, + "node_modules/@interchainjs/types": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/types/-/types-1.17.1.tgz", + "integrity": "sha512-EPKa0nFPjYFpgO+a5WUBXrkHbbBeYHyBv8DG55yTFuvvn4Yd6uH86ygQIR9NsEAMgKQSiTD+yQcVj/4fbC/EQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decimal.js": "^10.4.3" + } + }, + "node_modules/@interchainjs/utils": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/utils/-/utils-1.17.1.tgz", + "integrity": "sha512-46W53v0tRMHiThvF8IZbURqAJsCxNwdxMdKFlL1pDwqAUHxgFzM6IfxemijbNCQd2ST9EYKe9c5zmnxEsHioig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@interchainjs/types": "1.17.1", + "bech32": "^2.0.0", + "decimal.js": "^10.4.3" + }, + "peerDependencies": { + "@chain-registry/v2": "^1.71.186", + "@chain-registry/v2-types": "^0.53.115" + } + }, + "node_modules/@interchainjs/utils/node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" }, "engines": { - "node": ">=8" + "node": "20 || >=22" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/schemas": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", - "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, + "license": "ISC", "dependencies": { - "@sinclair/typebox": "^0.24.1" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": ">=12" } }, - "node_modules/@jest/transform": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-28.1.3.tgz", - "integrity": "sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==", + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^28.1.3", - "@jridgewell/trace-mapping": "^0.3.13", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.3", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.3", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.1" - }, + "license": "MIT", "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/transform/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@jest/transform/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@jest/transform/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/transform/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@jest/transform/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/transform/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/types": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", - "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^28.1.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/types/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/types/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@jest/types/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/types/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -2283,6 +1122,59 @@ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "dev": true }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pyramation/json-schema-ref-parser": { "version": "9.0.6", "resolved": "https://registry.npmjs.org/@pyramation/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz", @@ -2337,6 +1229,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2383,11 +1276,44 @@ "node": "*" } }, - "node_modules/@sinclair/typebox": { - "version": "0.24.51", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", - "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", - "dev": true + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } }, "node_modules/@types/glob": { "version": "7.2.0", @@ -2399,39 +1325,6 @@ "@types/node": "*" } }, - "node_modules/@types/graceful-fs": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", - "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, "node_modules/@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", @@ -2462,20 +1355,27 @@ "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", "dev": true }, - "node_modules/@types/yargs": { - "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", - "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "node_modules/@types/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==", "dev": true, + "license": "MIT", "dependencies": { - "@types/yargs-parser": "*" + "@types/glob": "*", + "@types/node": "*" } }, - "node_modules/@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", - "dev": true + "node_modules/@types/shelljs": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.8.15.tgz", + "integrity": "sha512-vzmnCHl6hViPu9GNLQJ+DZFd6BQI2DBTUeOvYHqkWQLMfKAAQYMb/xAmZkTogZI/vqXHCWkqDRymDI5p0QTi5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/glob": "~7.2.0", + "@types/node": "*" + } }, "node_modules/abbrev": { "version": "1.1.1", @@ -2581,19 +1481,21 @@ } }, "node_modules/ast-stringify": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ast-stringify/-/ast-stringify-0.1.0.tgz", - "integrity": "sha512-J1PgFYV3RG6r37+M6ySZJH406hR82okwGvFM9hLXpOvdx4WC4GEW8/qiw6pi1hKTrqcRvoHP8a7mp87egYr6iA==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ast-stringify/-/ast-stringify-0.2.1.tgz", + "integrity": "sha512-wS/5cTSysM/IL1NVAwI2/xhFDI08UEw4PpLsbpDfc1KEPc2ABo94pjvB5p/tIF+Qu7oFOdxx8TdlGhyu4ZOb3A==", "dev": true, - "dependencies": { - "@babel/runtime": "^7.11.2" - } + "license": "SEE LICENSE IN LICENSE" }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2601,67 +1503,50 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "^6.1.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.3.tgz", - "integrity": "sha512-zKsXDh0XjnrUEW0mxIHLfjBfnXSMr5Q/goMe/fxpQnLm07mcOZiIZHBNWCMx60HmdvjxfXcalac0tfFg0wqxyw==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.2", - "core-js-compat": "^3.21.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -2671,6 +1556,23 @@ "node": ">=8" } }, + "node_modules/bip39": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", + "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@noble/hashes": "^1.2.0" + } + }, + "node_modules/bn.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "dev": true, + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -2692,55 +1594,58 @@ "node": ">=8" } }, - "node_modules/browserslist": { - "version": "4.21.10", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", - "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001517", - "electron-to-chromium": "^1.4.477", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.11" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "dependencies": { - "node-int64": "^0.4.0" - } + "license": "MIT" }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2752,40 +1657,12 @@ "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", "dev": true }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001522", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001522.tgz", - "integrity": "sha512-TKiyTVZxJGhsTszLuzb+6vUZSjVOAhClszBr2Ta2k9IwtNBT/4dzmL6aywt0HCgEZlmwJzXJd8yNiob6HgwTRg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, "node_modules/case": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/case/-/case-1.6.3.tgz", "integrity": "sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ==", "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", "engines": { "node": ">= 0.8.0" } @@ -2837,21 +1714,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, "node_modules/cli-color": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.3.tgz", @@ -2925,25 +1787,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, - "node_modules/core-js-compat": { - "version": "3.32.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.1.tgz", - "integrity": "sha512-GSvKDv4wE0bPnQtjklV101juQ85g6H3rm5PDP20mqlS5j0kXF3pP97YvAu5hl+uFHqMictp3b2VxOHljWMAtuA==", - "dev": true, - "dependencies": { - "browserslist": "^4.21.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -2989,12 +1832,13 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -3005,15 +1849,41 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -3058,11 +1928,27 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/dotty": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/dotty/-/dotty-0.1.2.tgz", - "integrity": "sha512-V0EWmKeH3DEhMwAZ+8ZB2Ao4OK6p++Z0hsDtZq3N0+0ZMVqkzrcEGROvOnZpLnvBg5PTNG23JEDLAm64gPaotQ==", - "dev": true + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" }, "node_modules/ee-first": { "version": "1.1.1", @@ -3070,11 +1956,35 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true }, - "node_modules/electron-to-chromium": { - "version": "1.4.499", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.499.tgz", - "integrity": "sha512-0NmjlYBLKVHva4GABWAaHuPJolnDuL0AhV3h1hES6rcLCWEIbRL6/8TghfsVwkx6TEroQVdliX7+aLysUpKvjw==", - "dev": true + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" }, "node_modules/encodeurl": { "version": "1.0.2", @@ -3147,6 +2057,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-set-tostringtag": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", @@ -3226,15 +2169,6 @@ "es6-symbol": "^3.1.1" } }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -3263,15 +2197,6 @@ "node": ">=4" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -3281,6 +2206,40 @@ "node": ">= 0.6" } }, + "node_modules/ethereum-cryptography": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-3.2.0.tgz", + "integrity": "sha512-Urr5YVsalH+Jo0sYkTkv1MyI9bLYZwW8BENZCeE1QYaTHETEYx0Nv/SVsWkSqpYrzweg6d8KMY1wTjH/1m/BIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/ciphers": "1.3.0", + "@noble/curves": "1.9.0", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0" + }, + "engines": { + "node": "^14.21.3 || >=16", + "npm": ">=9" + } + }, + "node_modules/ethereum-cryptography/node_modules/@noble/curves": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.0.tgz", + "integrity": "sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/event-emitter": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", @@ -3320,21 +2279,6 @@ "node": ">=4" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "dependencies": { - "bser": "2.1.1" - } - }, "node_modules/figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -3392,26 +2336,114 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, + "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/foreground-child/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "node_modules/foreground-child/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", "dependencies": { - "is-callable": "^1.1.3" + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, "node_modules/fresh": { @@ -3444,10 +2476,14 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -3485,37 +2521,43 @@ "node": ">= 0.6.0" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, "engines": { - "node": ">=8.0.0" + "node": ">= 0.4" } }, "node_modules/get-stdin": { @@ -3547,19 +2589,21 @@ } }, "node_modules/glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, + "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": ">=12" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3577,11 +2621,28 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -3602,12 +2663,13 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3671,12 +2733,13 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3695,10 +2758,11 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3707,12 +2771,13 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -3721,6 +2786,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -3761,15 +2862,6 @@ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3942,6 +3034,26 @@ "inquirer-autocomplete-prompt": "^0.11.1" } }, + "node_modules/interchainjs": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/interchainjs/-/interchainjs-1.17.1.tgz", + "integrity": "sha512-7i22pCVrW+5EKmC16NFoR0lvI0DQ7DQlOLynRY4WeMN5Kc0Y/hZ5M3TzbcE3NXbtv82ygxJYwNIlrL24+xpXpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@interchainjs/cosmos": "1.17.1", + "@interchainjs/cosmos-types": "1.17.1", + "@interchainjs/crypto": "1.17.1", + "@interchainjs/encoding": "1.17.1", + "@interchainjs/ethereum": "1.17.1", + "@interchainjs/math": "1.17.1", + "@interchainjs/pubkey": "1.17.1", + "@interchainjs/types": "1.17.1", + "@interchainjs/utils": "1.17.1", + "@noble/hashes": "^1.3.1", + "decimal.js": "^10.4.3" + } + }, "node_modules/internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -4210,12 +3322,13 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, + "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -4260,201 +3373,42 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" + "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-haste-map": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.3.tgz", - "integrity": "sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.3", - "jest-worker": "^28.1.3", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "funding": { + "url": "https://github.com/sponsors/isaacs" }, "optionalDependencies": { - "fsevents": "^2.3.2" + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-util/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-util/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", - "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", - "dev": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } + "license": "MIT" }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -4468,6 +3422,7 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, @@ -4481,24 +3436,29 @@ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", "dev": true }, + "node_modules/libsodium-sumo": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.15.tgz", + "integrity": "sha512-5tPmqPmq8T8Nikpm1Nqj0hBHvsLFCXvdhBFV7SGOitQPZAA6jso8XoL0r4L7vmfKXr486fiQInvErHtEvizFMw==", + "dev": true, + "license": "ISC" + }, + "node_modules/libsodium-wrappers-sumo": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.15.tgz", + "integrity": "sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA==", + "dev": true, + "license": "ISC", + "dependencies": { + "libsodium-sumo": "^0.7.15" + } + }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -4514,44 +3474,18 @@ "node": ">=4" } }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true - }, - "node_modules/long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", - "dev": true - }, "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } + "license": "ISC" }, "node_modules/lru-queue": { "version": "0.1.0", @@ -4568,15 +3502,6 @@ "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", "dev": true }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "dependencies": { - "tmpl": "1.0.5" - } - }, "node_modules/marked": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", @@ -4589,6 +3514,16 @@ "node": ">= 12" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/memoizee": { "version": "0.4.15", "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", @@ -4614,25 +3549,6 @@ "node": ">= 0.10.0" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -4654,16 +3570,34 @@ "node": ">=4" } }, - "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", "dev": true, + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": ">=10" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -4672,6 +3606,16 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -4685,10 +3629,11 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, "node_modules/mute-stream": { "version": "0.0.7", @@ -4707,6 +3652,13 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nested-obj": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/nested-obj/-/nested-obj-0.0.1.tgz", + "integrity": "sha512-kB1WKTng+IePQhZVs1UXtFaHBx4QEM5a0XKGAzYfCKvdx5DhNjCytNDWMUGpNNpHLotln+tiwcA52kWCIgGq1Q==", + "dev": true, + "license": "SEE LICENSE IN LICENSE" + }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", @@ -4719,18 +3671,6 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true - }, - "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", - "dev": true - }, "node_modules/nodemon": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", @@ -5019,41 +3959,12 @@ "node": ">=0.10.0" } }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } + "license": "BlueOak-1.0.0" }, "node_modules/parse-json": { "version": "4.0.0", @@ -5083,15 +3994,6 @@ "node": ">= 0.8" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -5116,6 +4018,23 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-type": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", @@ -5129,10 +4048,11 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -5167,13 +4087,14 @@ "node": ">=4" } }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 6" + "node": ">= 0.4" } }, "node_modules/prettier": { @@ -5232,6 +4153,13 @@ "node": ">=8.10.0" } }, + "node_modules/readonly-date": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/readonly-date/-/readonly-date-1.0.0.tgz", + "integrity": "sha512-tMKIV7hlk0h4mO3JTmmVuIlJVXjKk3Sep9Bf5OH0O+758ruuVkUy2J9SttDLm91IEX/WHlXPSpxMGjPj4beMIQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -5244,39 +4172,6 @@ "node": ">= 0.10" } }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", - "dev": true, - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", - "dev": true - }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", @@ -5294,44 +4189,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", - "dev": true, - "dependencies": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dev": true, - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - } - }, "node_modules/reload": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/reload/-/reload-3.2.1.tgz", @@ -5368,15 +4225,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -5447,6 +4295,16 @@ "node": "*" } }, + "node_modules/rlp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rlp/-/rlp-3.0.0.tgz", + "integrity": "sha512-PD6U2PGk6Vq2spfgiWZdomLvRGDreBLxi5jv5M8EpRo3pU6VEm31KO+HFxE18Q3vgqfDrQ9pZA3FP95rkijNKw==", + "dev": true, + "license": "MPL-2.0", + "bin": { + "rlp": "bin/rlp" + } + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -5501,6 +4359,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-regex-test": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", @@ -5521,15 +4400,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -5569,12 +4439,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -5590,12 +4454,51 @@ "node": ">= 0.8.0" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -5762,15 +4665,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -5831,6 +4725,55 @@ "node": ">=4" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/ansi-regex": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", @@ -5926,6 +4869,30 @@ "node": ">=6" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -5972,62 +4939,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6077,17 +4988,27 @@ "node": ">=0.6.0" } }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -6138,14 +5059,15 @@ "dev": true }, "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -6243,6 +5165,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6272,46 +5195,6 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -6321,36 +5204,6 @@ "node": ">= 0.8" } }, - "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/util": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", @@ -6388,29 +5241,6 @@ "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", "dev": true }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/wasm-ast-types": { - "version": "0.26.2", - "resolved": "https://registry.npmjs.org/wasm-ast-types/-/wasm-ast-types-0.26.2.tgz", - "integrity": "sha512-DM34W0Sexd/BreN4g1ZjfaDC8hkKIw3FykV7vT9KdhCoroH+Qncz+jvSEI0fEDqbtJkWiP9Z+u61a5LPObHKaA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.18.9", - "@babel/types": "7.18.10", - "@jest/transform": "28.1.3", - "ast-stringify": "0.1.0", - "case": "1.6.3", - "deepmerge": "4.2.2" - } - }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -6440,16 +5270,19 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", - "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, + "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -6458,25 +5291,200 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/ws": { "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", @@ -6497,12 +5505,6 @@ "optional": true } } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true } } } From 7352499328fe2ae6bfbba285ac4a3bcd493aa649 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:19:30 +0000 Subject: [PATCH 158/180] Bump golang.org/x/crypto in /nym-gateway-probe/netstack_ping (#6220) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.39.0 to 0.45.0. - [Commits](https://github.com/golang/crypto/compare/v0.39.0...v0.45.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.45.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- nym-gateway-probe/netstack_ping/go.mod | 6 +++--- nym-gateway-probe/netstack_ping/go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/nym-gateway-probe/netstack_ping/go.mod b/nym-gateway-probe/netstack_ping/go.mod index ac45bca5eb..1845deeb3a 100644 --- a/nym-gateway-probe/netstack_ping/go.mod +++ b/nym-gateway-probe/netstack_ping/go.mod @@ -4,15 +4,15 @@ go 1.24.4 require ( github.com/amnezia-vpn/amneziawg-go v0.2.13 - golang.org/x/net v0.41.0 + golang.org/x/net v0.47.0 ) require ( github.com/google/btree v1.1.3 // indirect github.com/tevino/abool v1.2.0 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/crypto v0.39.0 // indirect - golang.org/x/sys v0.33.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/sys v0.38.0 // indirect golang.org/x/time v0.9.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect gvisor.dev/gvisor v0.0.0-20250611222258-0fe9a4bf489c // indirect diff --git a/nym-gateway-probe/netstack_ping/go.sum b/nym-gateway-probe/netstack_ping/go.sum index f43212422f..c9a61712c7 100644 --- a/nym-gateway-probe/netstack_ping/go.sum +++ b/nym-gateway-probe/netstack_ping/go.sum @@ -12,12 +12,12 @@ github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA= github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= From 9714351fd8707bb9b0e31ff65a1428d4fd1d5c25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:19:48 +0000 Subject: [PATCH 159/180] Bump glob from 10.3.4 to 10.5.0 in /documentation/scripts/post-process (#6216) Bumps [glob](https://github.com/isaacs/node-glob) from 10.3.4 to 10.5.0. - [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md) - [Commits](https://github.com/isaacs/node-glob/compare/v10.3.4...v10.5.0) --- updated-dependencies: - dependency-name: glob dependency-version: 10.5.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../scripts/post-process/package-lock.json | 136 +++++++++++------- .../scripts/post-process/package.json | 2 +- 2 files changed, 84 insertions(+), 54 deletions(-) diff --git a/documentation/scripts/post-process/package-lock.json b/documentation/scripts/post-process/package-lock.json index 8fcd8df5e6..34bedd213a 100644 --- a/documentation/scripts/post-process/package-lock.json +++ b/documentation/scripts/post-process/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "@jsdevtools/rehype-url-inspector": "^2.0.2", - "glob": "^10.3.4", + "glob": "^10.5.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "to-vfile": "^8.0.0", @@ -20,6 +20,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -48,6 +49,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", "optional": true, "engines": { "node": ">=14" @@ -80,9 +82,10 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, "node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -91,9 +94,10 @@ } }, "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -113,12 +117,14 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -154,6 +160,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -164,7 +171,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" }, "node_modules/comma-separated-tokens": { "version": "2.0.3", @@ -211,12 +219,14 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" }, "node_modules/entities": { "version": "4.5.0", @@ -245,21 +255,20 @@ } }, "node_modules/glob": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", - "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { - "glob": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=16 || 14 >=14.17" + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -527,6 +536,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", "engines": { "node": ">=8" } @@ -548,15 +558,13 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/jackspeak": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.3.tgz", - "integrity": "sha512-R2bUw+kVZFS/h1AZqBKrSgDmdmjApzgY0AlCPumopFiAlbUxE2gf+SCuBzQ0cP5hHmUmFYF5yw55T97Th5Kstg==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -565,12 +573,10 @@ } }, "node_modules/lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", - "engines": { - "node": "14 || >=16.14" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" }, "node_modules/mdast-util-to-hast": { "version": "13.0.2", @@ -676,9 +682,10 @@ ] }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -690,13 +697,20 @@ } }, "node_modules/minipass": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", - "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -717,15 +731,16 @@ } }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -811,6 +826,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -828,6 +844,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -841,6 +858,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -848,12 +866,14 @@ "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -875,9 +895,10 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -893,6 +914,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -904,6 +926,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -1220,6 +1243,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -1237,6 +1261,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -1253,6 +1278,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -1261,6 +1287,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -1274,12 +1301,14 @@ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -1293,6 +1322,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, diff --git a/documentation/scripts/post-process/package.json b/documentation/scripts/post-process/package.json index 04e3a762a2..1c31b9c78c 100644 --- a/documentation/scripts/post-process/package.json +++ b/documentation/scripts/post-process/package.json @@ -7,7 +7,7 @@ }, "dependencies": { "@jsdevtools/rehype-url-inspector": "^2.0.2", - "glob": "^10.3.4", + "glob": "^10.5.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "to-vfile": "^8.0.0", From 670f383faaffc4f71e38fdda5ffe71b79d9458d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:20:05 +0000 Subject: [PATCH 160/180] Bump js-yaml in /sdk/typescript/tests/integration-tests/mix-fetch (#6215) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.1.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../tests/integration-tests/mix-fetch/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/typescript/tests/integration-tests/mix-fetch/package-lock.json b/sdk/typescript/tests/integration-tests/mix-fetch/package-lock.json index f3c7b6aad6..1941833704 100644 --- a/sdk/typescript/tests/integration-tests/mix-fetch/package-lock.json +++ b/sdk/typescript/tests/integration-tests/mix-fetch/package-lock.json @@ -642,9 +642,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" From 66c2454775125401e7c9f5d45446e1bb9a34f643 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:20:22 +0000 Subject: [PATCH 161/180] Bump min-document from 2.19.0 to 2.19.1 (#6181) Bumps [min-document](https://github.com/Raynos/min-document) from 2.19.0 to 2.19.1. - [Commits](https://github.com/Raynos/min-document/compare/v2.19.0...v2.19.1) --- updated-dependencies: - dependency-name: min-document dependency-version: 2.19.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 246c016a46..0fe96d803b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16349,9 +16349,9 @@ mimic-fn@^3.1.0: integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== min-document@^2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" - integrity sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ== + version "2.19.1" + resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.1.tgz#7083ad4bc8879a6eba6516688e9f5d91d32e2d23" + integrity sha512-8lqe85PkqQJzIcs2iD7xW/WSxcncC3/DPVbTOafKNJDIMXwGfwXS350mH4SJslomntN2iYtFBuC0yNO3CEap6g== dependencies: dom-walk "^0.1.0" From aa37bfb7ff220df0d5601633400f119796fb3a09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:20:42 +0000 Subject: [PATCH 162/180] build(deps): bump mikefarah/yq from 4.47.1 to 4.48.1 (#6107) Bumps [mikefarah/yq](https://github.com/mikefarah/yq) from 4.47.1 to 4.48.1. - [Release notes](https://github.com/mikefarah/yq/releases) - [Changelog](https://github.com/mikefarah/yq/blob/master/release_notes.txt) - [Commits](https://github.com/mikefarah/yq/compare/v4.47.1...v4.48.1) --- updated-dependencies: - dependency-name: mikefarah/yq dependency-version: 4.48.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-check-ns-api-version.yml | 2 +- .github/workflows/ci-check-nym-stats-api-version.yml | 2 +- .github/workflows/push-credential-proxy.yaml | 2 +- .github/workflows/push-data-observatory.yaml | 2 +- .github/workflows/push-network-monitor.yaml | 2 +- .github/workflows/push-nym-api.yaml | 2 +- .github/workflows/push-nym-node.yaml | 2 +- .github/workflows/push-nym-statistics-api.yaml | 2 +- .github/workflows/push-nyx-chain-watcher.yaml | 2 +- .github/workflows/push-validator-rewarder.yaml | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci-check-ns-api-version.yml b/.github/workflows/ci-check-ns-api-version.yml index 322024c0a5..42eded2c86 100644 --- a/.github/workflows/ci-check-ns-api-version.yml +++ b/.github/workflows/ci-check-ns-api-version.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v4 - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/ci-check-nym-stats-api-version.yml b/.github/workflows/ci-check-nym-stats-api-version.yml index 130bb841d2..ac46925ed3 100644 --- a/.github/workflows/ci-check-nym-stats-api-version.yml +++ b/.github/workflows/ci-check-nym-stats-api-version.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v4 - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/push-credential-proxy.yaml b/.github/workflows/push-credential-proxy.yaml index 0c9b7952ac..b8337a58bb 100644 --- a/.github/workflows/push-credential-proxy.yaml +++ b/.github/workflows/push-credential-proxy.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Cargo.toml diff --git a/.github/workflows/push-data-observatory.yaml b/.github/workflows/push-data-observatory.yaml index bd999f45d5..201708d3bd 100644 --- a/.github/workflows/push-data-observatory.yaml +++ b/.github/workflows/push-data-observatory.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/push-network-monitor.yaml b/.github/workflows/push-network-monitor.yaml index 8baa371411..48990a6789 100644 --- a/.github/workflows/push-network-monitor.yaml +++ b/.github/workflows/push-network-monitor.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-network-monitor/Cargo.toml diff --git a/.github/workflows/push-nym-api.yaml b/.github/workflows/push-nym-api.yaml index 79f5fe9ade..7539343f4e 100644 --- a/.github/workflows/push-nym-api.yaml +++ b/.github/workflows/push-nym-api.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-api/Cargo.toml diff --git a/.github/workflows/push-nym-node.yaml b/.github/workflows/push-nym-node.yaml index 05ad829163..3397171354 100644 --- a/.github/workflows/push-nym-node.yaml +++ b/.github/workflows/push-nym-node.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/push-nym-statistics-api.yaml b/.github/workflows/push-nym-statistics-api.yaml index 3bb9256dfa..c51e3f59e7 100644 --- a/.github/workflows/push-nym-statistics-api.yaml +++ b/.github/workflows/push-nym-statistics-api.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/push-nyx-chain-watcher.yaml b/.github/workflows/push-nyx-chain-watcher.yaml index c156a6ecde..5fb76e89af 100644 --- a/.github/workflows/push-nyx-chain-watcher.yaml +++ b/.github/workflows/push-nyx-chain-watcher.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/push-validator-rewarder.yaml b/.github/workflows/push-validator-rewarder.yaml index 7fcb4b166e..9f8e7ab32c 100644 --- a/.github/workflows/push-validator-rewarder.yaml +++ b/.github/workflows/push-validator-rewarder.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml From 9c6310264ea94bb359691b7ad9c3c3e5c5272a9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:21:06 +0000 Subject: [PATCH 163/180] build(deps): bump tar-fs (#6063) Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 3.0.9 to 3.1.1. - [Commits](https://github.com/mafintosh/tar-fs/compare/v3.0.9...v3.1.1) --- updated-dependencies: - dependency-name: tar-fs dependency-version: 3.1.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../tests/integration-tests/mix-fetch/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/typescript/tests/integration-tests/mix-fetch/package-lock.json b/sdk/typescript/tests/integration-tests/mix-fetch/package-lock.json index 1941833704..02138582d9 100644 --- a/sdk/typescript/tests/integration-tests/mix-fetch/package-lock.json +++ b/sdk/typescript/tests/integration-tests/mix-fetch/package-lock.json @@ -1058,9 +1058,9 @@ } }, "node_modules/tar-fs": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz", - "integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "license": "MIT", "dependencies": { "pump": "^3.0.0", From 0ab14f704174d1a35c36249eef8f1d2e9ea7e0fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:21:30 +0000 Subject: [PATCH 164/180] build(deps): bump ammonia from 4.1.1 to 4.1.2 (#6057) Bumps [ammonia](https://github.com/rust-ammonia/ammonia) from 4.1.1 to 4.1.2. - [Release notes](https://github.com/rust-ammonia/ammonia/releases) - [Changelog](https://github.com/rust-ammonia/ammonia/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-ammonia/ammonia/compare/v4.1.1...v4.1.2) --- updated-dependencies: - dependency-name: ammonia dependency-version: 4.1.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 6dd3b78a74b260ef5a9544d4a39e8ac4e3783354 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:21:57 +0000 Subject: [PATCH 165/180] build(deps): bump actions/setup-go from 5 to 6 (#6013) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-sdk-wasm.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-sdk-wasm.yml b/.github/workflows/ci-sdk-wasm.yml index 2bb2c52d83..3f8d57ad4b 100644 --- a/.github/workflows/ci-sdk-wasm.yml +++ b/.github/workflows/ci-sdk-wasm.yml @@ -31,7 +31,7 @@ jobs: components: rustfmt, clippy - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: "1.24.6" From 31ff3645c52920b532df27db708d781b8cb674f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:22:50 +0000 Subject: [PATCH 166/180] build(deps): bump actions/upload-pages-artifact from 3 to 4 (#5992) Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 3 to 4. - [Release notes](https://github.com/actions/upload-pages-artifact/releases) - [Commits](https://github.com/actions/upload-pages-artifact/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/upload-pages-artifact dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy-github-pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-github-pages.yml b/.github/workflows/deploy-github-pages.yml index f1e47a3ed8..6b98e9e33f 100644 --- a/.github/workflows/deploy-github-pages.yml +++ b/.github/workflows/deploy-github-pages.yml @@ -34,7 +34,7 @@ jobs: - name: Setup Pages uses: actions/configure-pages@v5 - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v4 with: # Upload entire repository path: './ppa' From dac7f1f83cf5f3c9745c051cbbc6a701deb36128 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:25:52 +0000 Subject: [PATCH 167/180] build(deps): bump tracing-subscriber in /nym-wallet (#5994) Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.19 to 0.3.20. - [Release notes](https://github.com/tokio-rs/tracing/releases) - [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.19...tracing-subscriber-0.3.20) --- updated-dependencies: - dependency-name: tracing-subscriber dependency-version: 0.3.20 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.toml | 2 +- nym-wallet/Cargo.lock | 46 +++++++++++-------------------------------- 2 files changed, 13 insertions(+), 35 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 92f5fdbb7b..7f0e5938b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -351,7 +351,7 @@ tower-http = "0.5.2" tracing = "0.1.41" tracing-log = "0.2" tracing-opentelemetry = "0.19.0" -tracing-subscriber = "0.3.19" +tracing-subscriber = "0.3.20" tracing-tree = "0.2.2" tracing-indicatif = "0.3.9" tracing-test = "0.2.5" diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 4f910e1a15..19db598a32 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3860,11 +3860,11 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -4051,12 +4051,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -5061,12 +5060,6 @@ dependencies = [ "thiserror 2.0.12", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "p256" version = "0.13.2" @@ -5953,17 +5946,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -5974,15 +5958,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.5" @@ -7951,14 +7929,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", From ae76335c31e3bbfef5c27ac7d0aa6e7e18152762 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:26:12 +0000 Subject: [PATCH 168/180] build(deps): bump tracing-subscriber from 0.3.19 to 0.3.20 (#5993) Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.19 to 0.3.20. - [Release notes](https://github.com/tokio-rs/tracing/releases) - [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.19...tracing-subscriber-0.3.20) --- updated-dependencies: - dependency-name: tracing-subscriber dependency-version: 0.3.20 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 46 ++++++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b7f0f8fe6e..9d0e92c79a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4374,11 +4374,11 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -4741,6 +4741,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -8810,17 +8819,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -8831,15 +8831,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.5" @@ -11202,14 +11196,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", - "nu-ansi-term", + "nu-ansi-term 0.50.1", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -11245,7 +11239,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ec6adcab41b1391b08a308cc6302b79f8095d1673f6947c2dc65ffb028b0b2d" dependencies = [ - "nu-ansi-term", + "nu-ansi-term 0.46.0", "tracing-core", "tracing-log 0.1.4", "tracing-subscriber", From e410aecf40c9311c77ce34bcfe1a5c66e8e8f144 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:43:33 +0000 Subject: [PATCH 169/180] build(deps): bump tower-http from 0.5.2 to 0.6.6 (#6030) Bumps [tower-http](https://github.com/tower-rs/tower-http) from 0.5.2 to 0.6.6. - [Release notes](https://github.com/tower-rs/tower-http/releases) - [Commits](https://github.com/tower-rs/tower-http/compare/tower-http-0.5.2...tower-http-0.6.6) --- updated-dependencies: - dependency-name: tower-http dependency-version: 0.6.6 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 54 +++++++++++++++++++----------------------------------- Cargo.toml | 2 +- 2 files changed, 20 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d0e92c79a..65d9405cab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1283,7 +1283,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -3965,7 +3965,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4905,7 +4905,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-util", - "tower-http 0.5.2", + "tower-http", "tracing", "ts-rs", "url", @@ -5487,7 +5487,7 @@ dependencies = [ "time", "tokio", "tokio-util", - "tower-http 0.5.2", + "tower-http", "tracing", "url", "utoipa", @@ -6491,7 +6491,7 @@ dependencies = [ "tokio-stream", "tokio-util", "toml 0.8.23", - "tower-http 0.5.2", + "tower-http", "tracing", "tracing-indicatif", "tracing-subscriber", @@ -6609,7 +6609,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-util", - "tower-http 0.5.2", + "tower-http", "tracing", "tracing-log 0.2.0", "tracing-subscriber", @@ -7225,7 +7225,7 @@ dependencies = [ "time", "tokio", "tokio-util", - "tower-http 0.5.2", + "tower-http", "tracing", "tracing-subscriber", "url", @@ -7623,7 +7623,7 @@ dependencies = [ "nym-wireguard-private-metadata-shared", "tokio", "tokio-util", - "tower-http 0.5.2", + "tower-http", "utoipa", "utoipa-swagger-ui", ] @@ -7661,7 +7661,7 @@ dependencies = [ "time", "tokio", "tower 0.5.2", - "tower-http 0.5.2", + "tower-http", "utoipa", ] @@ -7730,7 +7730,7 @@ dependencies = [ "time", "tokio", "tokio-util", - "tower-http 0.5.2", + "tower-http", "tracing", "tracing-subscriber", "utoipa", @@ -8658,7 +8658,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -8913,7 +8913,7 @@ dependencies = [ "tokio-rustls 0.26.2", "tokio-util", "tower 0.5.2", - "tower-http 0.6.6", + "tower-http", "tower-service", "url", "wasm-bindgen", @@ -9113,7 +9113,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -10429,7 +10429,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.0.8", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -11048,9 +11048,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.5.2" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "async-compression", "bitflags 2.9.1", @@ -11062,33 +11062,17 @@ dependencies = [ "http-body-util", "http-range-header", "httpdate", + "iri-string", "mime", "mime_guess", "percent-encoding", "pin-project-lite", "tokio", "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags 2.9.1", - "bytes", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "iri-string", - "pin-project-lite", "tower 0.5.2", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -12241,7 +12225,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7f0e5938b5..eaad10bbc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -347,7 +347,7 @@ tokio-tungstenite = { version = "0.20.1" } tokio-util = "0.7.15" toml = "0.8.22" tower = "0.5.2" -tower-http = "0.5.2" +tower-http = "0.6.6" tracing = "0.1.41" tracing-log = "0.2" tracing-opentelemetry = "0.19.0" From ca7cbac320b729741f14eae97d986d712ab7c9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 26 Nov 2025 10:45:01 +0000 Subject: [PATCH 170/180] chore: don't rederive wallet keys on every tx (#6213) * chore: make 'DirectSecp256k1HdWallet' only derive its keys once on construction Previously all the keys and account information was being derived for every transaction signed * no longer keep account seed on the wallet struct --- Cargo.lock | 1 + .../client-libs/validator-client/Cargo.toml | 1 + .../examples/offline_signing.rs | 17 ++- .../validator-client/src/client.rs | 15 ++- .../client-libs/validator-client/src/error.rs | 7 + .../client-libs/validator-client/src/lib.rs | 5 + .../contract_traits/dkg_signing_client.rs | 2 +- .../contract_traits/ecash_signing_client.rs | 2 +- .../contract_traits/group_signing_client.rs | 2 +- .../contract_traits/mixnet_signing_client.rs | 2 +- .../multisig_signing_client.rs | 2 +- .../performance_signing_client.rs | 2 +- .../contract_traits/vesting_signing_client.rs | 2 +- .../src/nyxd/cosmwasm_client/mod.rs | 2 +- .../validator-client/src/nyxd/mod.rs | 25 ++-- .../validator-client/src/rpc/reqwest.rs | 4 + .../src/signing/direct_wallet.rs | 125 +++++++----------- .../validator-client/src/signing/mod.rs | 56 +++++++- .../validator-client/src/signing/signer.rs | 29 ++-- .../validator-client/src/signing/tx_signer.rs | 2 +- .../commands/src/validator/account/create.rs | 6 +- .../commands/src/validator/account/pubkey.rs | 31 +++-- .../commands/src/validator/signature/sign.rs | 51 +++---- common/types/src/error.rs | 4 + .../src/operations/mixnet/account.rs | 8 +- .../src/operations/signatures/sign.rs | 2 +- .../testnet-manager/src/manager/contract.rs | 10 +- 27 files changed, 234 insertions(+), 181 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 65d9405cab..b507cb04de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7406,6 +7406,7 @@ dependencies = [ name = "nym-validator-client" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "base64 0.22.1", "bip32", diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index afb988c98c..ec4eb62bec 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -75,6 +75,7 @@ workspace = true features = ["json", "rustls-tls"] [dev-dependencies] +anyhow = { workspace = true } bip39 = { workspace = true } cosmrs = { workspace = true, features = ["bip32"] } ts-rs = { workspace = true } diff --git a/common/client-libs/validator-client/examples/offline_signing.rs b/common/client-libs/validator-client/examples/offline_signing.rs index 62c160f1a8..c0a8295307 100644 --- a/common/client-libs/validator-client/examples/offline_signing.rs +++ b/common/client-libs/validator-client/examples/offline_signing.rs @@ -7,6 +7,7 @@ use cosmrs::{tx, AccountId, Coin, Denom}; use nym_validator_client::http_client; use nym_validator_client::nyxd::CosmWasmClient; use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet; +use nym_validator_client::signing::signer::OfflineSigner; use nym_validator_client::signing::tx_signer::TxSigner; use nym_validator_client::signing::SignerData; @@ -19,8 +20,8 @@ async fn main() { let validator = "https://rpc.sandbox.nymtech.net"; let to_address: AccountId = "n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa".parse().unwrap(); - let signer = DirectSecp256k1HdWallet::from_mnemonic(prefix, signer_mnemonic); - let signer_address = signer.try_derive_accounts().unwrap()[0].address().clone(); + let signer = DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, signer_mnemonic).unwrap(); + let signer_address = signer.signer_addresses()[0].clone(); // local 'client' ONLY signing messages let tx_signer = signer; @@ -57,9 +58,15 @@ async fn main() { 100000u32, ); - let tx_raw = tx_signer - .sign_direct(&signer_address, vec![send_msg], fee, memo, signer_data) - .unwrap(); + let tx_raw = TxSigner::sign_direct( + &tx_signer, + &signer_address, + vec![send_msg], + fee, + memo, + signer_data, + ) + .unwrap(); let tx_bytes = tx_raw.to_bytes().unwrap(); // compare balances from before and after the tx diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 2758369ede..eb23ba60ca 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -5,8 +5,7 @@ use crate::nyxd::{self, NyxdClient}; use crate::signing::direct_wallet::DirectSecp256k1HdWallet; use crate::signing::signer::{NoSigner, OfflineSigner}; use crate::{ - DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient, ReqwestRpcClient, - ValidatorClientError, + DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient, ValidatorClientError, }; use nym_api_requests::ecash::models::{ AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, @@ -164,7 +163,7 @@ impl Client { ) -> Result { let rpc_client = http_client(config.nyxd_url.as_str())?; let prefix = &config.nyxd_config.chain_details.bech32_account_prefix; - let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic); + let wallet = DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, mnemonic)?; Ok(Self::new_signing_with_rpc_client( config, rpc_client, wallet, @@ -177,12 +176,13 @@ impl Client { } } -impl Client { +#[allow(deprecated)] +impl Client { pub fn new_reqwest_signing( config: Config, mnemonic: bip39::Mnemonic, ) -> DirectSigningReqwestRpcValidatorClient { - let rpc_client = ReqwestRpcClient::new(config.nyxd_url.clone()); + let rpc_client = crate::ReqwestRpcClient::new(config.nyxd_url.clone()); let prefix = &config.nyxd_config.chain_details.bech32_account_prefix; let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic); @@ -203,9 +203,10 @@ impl Client { } } -impl Client { +#[allow(deprecated)] +impl Client { pub fn new_reqwest_query(config: Config) -> QueryReqwestRpcValidatorClient { - let rpc_client = ReqwestRpcClient::new(config.nyxd_url.clone()); + let rpc_client = crate::ReqwestRpcClient::new(config.nyxd_url.clone()); Self::new_with_rpc_client(config, rpc_client) } } diff --git a/common/client-libs/validator-client/src/error.rs b/common/client-libs/validator-client/src/error.rs index c3979c0784..09fb7c8171 100644 --- a/common/client-libs/validator-client/src/error.rs +++ b/common/client-libs/validator-client/src/error.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::nym_api; +use crate::signing::direct_wallet::DirectSecp256k1HdWalletError; pub use tendermint_rpc::error::Error as TendermintRpcError; use thiserror::Error; @@ -26,6 +27,12 @@ pub enum ValidatorClientError { #[error("No validator API url has been provided")] NoAPIUrlAvailable, + + #[error("failed to derive signing accounts: {source}")] + AccountDerivationFailure { + #[from] + source: DirectSecp256k1HdWalletError, + }, } impl From for ValidatorClientError { diff --git a/common/client-libs/validator-client/src/lib.rs b/common/client-libs/validator-client/src/lib.rs index db86658047..1736616dda 100644 --- a/common/client-libs/validator-client/src/lib.rs +++ b/common/client-libs/validator-client/src/lib.rs @@ -12,6 +12,7 @@ pub mod rpc; pub mod signing; pub use crate::error::ValidatorClientError; +#[allow(deprecated)] pub use crate::rpc::reqwest::ReqwestRpcClient; pub use crate::signing::direct_wallet::DirectSecp256k1HdWallet; pub use client::{Client, Config, EcashApiClient}; @@ -38,9 +39,13 @@ pub type DirectSigningHttpRpcValidatorClient = Client; +#[allow(deprecated)] pub type QueryReqwestRpcValidatorClient = Client; +#[allow(deprecated)] pub type QueryReqwestRpcNyxdClient = nyxd::NyxdClient; +#[allow(deprecated)] pub type DirectSigningReqwestRpcValidatorClient = Client; +#[allow(deprecated)] pub type DirectSigningReqwestRpcNyxdClient = nyxd::NyxdClient; diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_signing_client.rs index 8371f33133..bebc6aab69 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_signing_client.rs @@ -178,7 +178,7 @@ where .ok_or_else(|| NyxdError::unavailable_contract_address("dkg contract"))?; let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); - let signer_address = &self.signer_addresses()?[0]; + let signer_address = &self.signer_addresses()[0]; self.execute(signer_address, dkg_contract_address, &msg, fee, memo, funds) .await diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_signing_client.rs index 4c40a2b2fb..72ba43053b 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_signing_client.rs @@ -99,7 +99,7 @@ where .ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?; let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); - let signer_address = &self.signer_addresses()?[0]; + let signer_address = &self.signer_addresses()[0]; self.execute( signer_address, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/group_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/group_signing_client.rs index 5ac1954849..8ce18625b0 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/group_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/group_signing_client.rs @@ -95,7 +95,7 @@ where let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); - let signer_address = &self.signer_addresses()?[0]; + let signer_address = &self.signer_addresses()[0]; self.execute( signer_address, group_contract_address, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs index 7ba4b3cd98..69aaac59d5 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs @@ -667,7 +667,7 @@ where let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); let memo = msg.default_memo(); - let signer_address = &self.signer_addresses()?[0]; + let signer_address = &self.signer_addresses()[0]; self.execute( signer_address, mixnet_contract_address, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs index 715a38d457..eefa56da36 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs @@ -133,7 +133,7 @@ where let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); - let signer_address = &self.signer_addresses()?[0]; + let signer_address = &self.signer_addresses()[0]; self.execute( signer_address, multisig_contract_address, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs index 78b960265a..d0380c0452 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs @@ -165,7 +165,7 @@ where let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); - let signer_address = &self.signer_addresses()?[0]; + let signer_address = &self.signer_addresses()[0]; self.execute( signer_address, performance_contract_address, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs index 2f50016f91..2a46cf04ac 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs @@ -375,7 +375,7 @@ where let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); let memo = msg.name().to_string(); - let signer_address = &self.signer_addresses()?[0]; + let signer_address = &self.signer_addresses()[0]; self.execute( signer_address, vesting_contract_address, diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs index 6516588339..df7f03dd34 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs @@ -324,7 +324,7 @@ where { type Error = S::Error; - fn get_accounts(&self) -> Result, Self::Error> { + fn get_accounts(&self) -> &[AccountData] { self.signer.get_accounts() } diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index 85b08ab12a..d7768b4bfd 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -19,7 +19,7 @@ use crate::signing::signer::NoSigner; use crate::signing::signer::OfflineSigner; use crate::signing::tx_signer::TxSigner; use crate::signing::AccountData; -use crate::{DirectSigningReqwestRpcNyxdClient, QueryReqwestRpcNyxdClient, ReqwestRpcClient}; +use crate::{DirectSigningReqwestRpcNyxdClient, QueryReqwestRpcNyxdClient}; use async_trait::async_trait; use cosmrs::tendermint::{abci, evidence::Evidence, Genesis}; use cosmrs::tx::{Raw, SignDoc}; @@ -158,12 +158,13 @@ impl NyxdClient { } } -impl NyxdClient { +#[allow(deprecated)] +impl NyxdClient { pub fn connect_reqwest( config: Config, endpoint: Url, ) -> Result { - let client = ReqwestRpcClient::new(endpoint); + let client = crate::ReqwestRpcClient::new(endpoint); Ok(NyxdClient { client: MaybeSigningClient::new(client, (&config).into()), @@ -195,18 +196,19 @@ impl NyxdClient { let client = http_client(endpoint)?; let prefix = &config.chain_details.bech32_account_prefix; - let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic); + let wallet = DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, mnemonic)?; Ok(Self::connect_with_signer(config, client, wallet)) } } -impl NyxdClient { +#[allow(deprecated)] +impl NyxdClient { pub fn connect_reqwest_with_mnemonic( config: Config, endpoint: Url, mnemonic: bip39::Mnemonic, ) -> DirectSigningReqwestRpcNyxdClient { - let client = ReqwestRpcClient::new(endpoint); + let client = crate::ReqwestRpcClient::new(endpoint); let prefix = &config.chain_details.bech32_account_prefix; let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic); @@ -391,17 +393,12 @@ where S: OfflineSigner + Send + Sync, NyxdError: From<::Error>, { - pub fn signing_account(&self) -> Result { + pub fn signing_account(&self) -> Result<&AccountData, NyxdError> { Ok(self.find_account(&self.address())?) } pub fn address(&self) -> AccountId { - match self.client.signer_addresses() { - Ok(addresses) => addresses[0].clone(), - Err(_) => { - panic!("key derivation failure") - } - } + self.client.signer_addresses()[0].clone() } pub fn mix_coin(&self, amount: u128) -> Coin { @@ -867,7 +864,7 @@ where { type Error = S::Error; - fn get_accounts(&self) -> Result, Self::Error> { + fn get_accounts(&self) -> &[AccountData] { self.client.get_accounts() } diff --git a/common/client-libs/validator-client/src/rpc/reqwest.rs b/common/client-libs/validator-client/src/rpc/reqwest.rs index 09312b28c3..b4cd98cd2f 100644 --- a/common/client-libs/validator-client/src/rpc/reqwest.rs +++ b/common/client-libs/validator-client/src/rpc/reqwest.rs @@ -42,12 +42,15 @@ macro_rules! perform_with_compat { }}; } +// the separate implementation is now completely redundant +#[deprecated(note = "use HttpClient directly instead")] pub struct ReqwestRpcClient { compat: CompatMode, inner: reqwest::Client, url: Url, } +#[allow(deprecated)] impl ReqwestRpcClient { pub fn new(url: Url) -> Self { ReqwestRpcClient { @@ -131,6 +134,7 @@ impl TendermintRpcErrorMap for reqwest::Error { } } +#[allow(deprecated)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl TendermintRpcClient for ReqwestRpcClient { diff --git a/common/client-libs/validator-client/src/signing/direct_wallet.rs b/common/client-libs/validator-client/src/signing/direct_wallet.rs index 9d44255129..6192ead995 100644 --- a/common/client-libs/validator-client/src/signing/direct_wallet.rs +++ b/common/client-libs/validator-client/src/signing/direct_wallet.rs @@ -3,17 +3,13 @@ use crate::signing::signer::{OfflineSigner, SigningError}; use crate::signing::{AccountData, Secp256k1Derivation}; -use cosmrs::bip32::{DerivationPath, XPrv}; -use cosmrs::crypto::secp256k1::SigningKey; -use cosmrs::crypto::PublicKey; +use cosmrs::bip32::DerivationPath; use cosmrs::tx; use cosmrs::tx::SignDoc; use nym_config::defaults; use thiserror::Error; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; -type Secp256k1Keypair = (SigningKey, PublicKey); - #[derive(Debug, Error)] pub enum DirectSecp256k1HdWalletError { #[error(transparent)] @@ -36,28 +32,22 @@ pub enum DirectSecp256k1HdWalletError { } // TODO: maybe lock this one behind feature flag? -#[derive(Debug, Clone, Zeroize, ZeroizeOnDrop)] +#[derive(Zeroize, ZeroizeOnDrop)] pub struct DirectSecp256k1HdWallet { /// Base secret secret: bip39::Mnemonic, - /// BIP39 seed - seed: [u8; 64], - - // An unfortunate result of immature rust async story is that async traits (only available in the separate package) - // can't yet figure out everything and if we stored our derived account data on the struct, - // that would include the secret key which is a dyn EcdsaSigner and hence not Sync making the wallet - // not Sync and if used on the signing client in an async trait, it wouldn't be Send - /// Derivation instructions + /// Derived accounts #[zeroize(skip)] - accounts: Vec, + // unfortunately `dyn EcdsaSigner` does not guarantee Zeroize + accounts: Vec, } impl OfflineSigner for DirectSecp256k1HdWallet { type Error = DirectSecp256k1HdWalletError; - fn get_accounts(&self) -> Result, Self::Error> { - self.try_derive_accounts() + fn get_accounts(&self) -> &[AccountData] { + &self.accounts } fn sign_direct_with_account( @@ -77,55 +67,27 @@ impl DirectSecp256k1HdWallet { } /// Restores a wallet from the given BIP39 mnemonic using default options. + #[deprecated( + note = "this function can potentially panic if accounts can't be derived correctly. please use .checked_from_mnemonic() instead" + )] pub fn from_mnemonic(prefix: &str, mnemonic: bip39::Mnemonic) -> Self { + // unfortunately due to backwards compatibility requirements, + // we can't change signature of this method + #[allow(deprecated)] DirectSecp256k1HdWalletBuilder::new(prefix).build(mnemonic) } + /// Restores a wallet from the given BIP39 mnemonic using default options. + pub fn checked_from_mnemonic( + prefix: &str, + mnemonic: bip39::Mnemonic, + ) -> Result { + DirectSecp256k1HdWalletBuilder::new(prefix).try_build(mnemonic) + } + pub fn generate(prefix: &str, word_count: usize) -> Result { let mneomonic = bip39::Mnemonic::generate(word_count)?; - Ok(Self::from_mnemonic(prefix, mneomonic)) - } - - fn derive_keypair( - &self, - hd_path: &DerivationPath, - ) -> Result { - let extended_private_key = XPrv::derive_from_path(self.seed, hd_path)?; - - let private_key: SigningKey = extended_private_key.into(); - let public_key = private_key.public_key(); - - Ok((private_key, public_key)) - } - - pub fn derive_extended_private_key( - &self, - hd_path: &DerivationPath, - ) -> Result { - Ok(XPrv::derive_from_path(self.seed, hd_path)?) - } - - pub fn try_derive_accounts(&self) -> Result, DirectSecp256k1HdWalletError> { - let mut accounts = Vec::with_capacity(self.accounts.len()); - for derivation_info in &self.accounts { - let keypair = self.derive_keypair(&derivation_info.hd_path)?; - - // it seems this can only fail if the provided account prefix is invalid - let address = keypair - .1 - .account_id(&derivation_info.prefix) - .map_err( - |source| DirectSecp256k1HdWalletError::AccountDerivationError { source }, - )?; - - accounts.push(AccountData { - address, - public_key: keypair.1, - private_key: keypair.0, - }) - } - - Ok(accounts) + Self::checked_from_mnemonic(prefix, mneomonic) } pub fn secret(&self) -> &bip39::Mnemonic { @@ -188,23 +150,39 @@ impl DirectSecp256k1HdWalletBuilder { self } + #[deprecated( + note = "this function can potentially panic if accounts can't be derived correctly. please use .try_build() instead" + )] pub fn build(self, mnemonic: bip39::Mnemonic) -> DirectSecp256k1HdWallet { - let seed = mnemonic.to_seed(&self.bip39_password); + // unfortunately due to backwards compatibility requirements, + // we can't change signature of this method + #[allow(clippy::expect_used)] + self.try_build(mnemonic) + .expect("account derivation failure") + } + + pub fn try_build( + self, + mnemonic: bip39::Mnemonic, + ) -> Result { + let seed = Zeroizing::new(mnemonic.to_seed(&self.bip39_password)); let prefix = self.prefix.clone(); let accounts = self .hd_paths .iter() - .map(|hd_path| Secp256k1Derivation { - hd_path: hd_path.clone(), - prefix: prefix.clone(), + .map(|hd_path| { + Secp256k1Derivation { + hd_path: hd_path.clone(), + prefix: prefix.clone(), + } + .try_derive_account(&seed) }) - .collect(); + .collect::>()?; - DirectSecp256k1HdWallet { + Ok(DirectSecp256k1HdWallet { accounts, - seed, secret: mnemonic, - } + }) } } @@ -215,7 +193,7 @@ mod tests { use super::*; #[test] - fn generating_account_addresses() { + fn generating_account_addresses() -> anyhow::Result<()> { // test vectors produced from our js wallet let mnemonics = ["crush minute paddle tobacco message debate cabin peace bar jacket execute twenty winner view sure mask popular couch penalty fragile demise fresh pizza stove", "acquire rebel spot skin gun such erupt pull swear must define ill chief turtle today flower chunk truth battle claw rigid detail gym feel", @@ -230,11 +208,10 @@ mod tests { "n17n9flp6jflljg6fp05dsy07wcprf2uuu8g40rf", ]; for (idx, mnemonic) in mnemonics.iter().enumerate() { - let wallet = DirectSecp256k1HdWallet::from_mnemonic(&prefix, mnemonic.parse().unwrap()); - assert_eq!( - wallet.try_derive_accounts().unwrap()[0].address, - addrs[idx].parse().unwrap() - ) + let wallet = + DirectSecp256k1HdWallet::checked_from_mnemonic(&prefix, mnemonic.parse()?)?; + assert_eq!(wallet.signer_addresses()[0], addrs[idx].parse().unwrap()); } + Ok(()) } } diff --git a/common/client-libs/validator-client/src/signing/mod.rs b/common/client-libs/validator-client/src/signing/mod.rs index d9fa0c30e4..e3f6174115 100644 --- a/common/client-libs/validator-client/src/signing/mod.rs +++ b/common/client-libs/validator-client/src/signing/mod.rs @@ -1,6 +1,8 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::signing::direct_wallet::DirectSecp256k1HdWalletError; +use bip32::XPrv; use cosmrs::bip32::DerivationPath; use cosmrs::crypto::secp256k1::SigningKey; use cosmrs::crypto::PublicKey; @@ -12,14 +14,64 @@ pub mod direct_wallet; pub mod signer; pub mod tx_signer; +pub(crate) type Secp256k1Keypair = (SigningKey, PublicKey); + /// Derivation information required to derive a keypair and an address from a mnemonic. #[derive(Debug, Clone)] -struct Secp256k1Derivation { +pub(crate) struct Secp256k1Derivation { hd_path: DerivationPath, prefix: String, } -// TODO: is this struct going to be derivable with other signer types? +impl Secp256k1Derivation { + pub(crate) fn try_derive_account( + &self, + seed: S, + ) -> Result + where + S: AsRef<[u8]>, + { + let keypair = derive_keypair(seed, &self.hd_path)?; + + // it seems this can only fail if the provided account prefix is invalid + let address = keypair + .1 + .account_id(&self.prefix) + .map_err(|source| DirectSecp256k1HdWalletError::AccountDerivationError { source })?; + + Ok(AccountData { + address, + public_key: keypair.1, + private_key: keypair.0, + }) + } +} + +fn derive_keypair( + seed: S, + hd_path: &DerivationPath, +) -> Result +where + S: AsRef<[u8]>, +{ + let extended_private_key = derive_extended_private_key(seed, hd_path)?; + + let private_key: SigningKey = extended_private_key.into(); + let public_key = private_key.public_key(); + + Ok((private_key, public_key)) +} + +fn derive_extended_private_key( + seed: S, + hd_path: &DerivationPath, +) -> Result +where + S: AsRef<[u8]>, +{ + Ok(XPrv::derive_from_path(seed, hd_path)?) +} + pub struct AccountData { pub address: AccountId, diff --git a/common/client-libs/validator-client/src/signing/signer.rs b/common/client-libs/validator-client/src/signing/signer.rs index 4b620291fe..af7fb584e0 100644 --- a/common/client-libs/validator-client/src/signing/signer.rs +++ b/common/client-libs/validator-client/src/signing/signer.rs @@ -33,23 +33,18 @@ pub enum SignerType { pub trait OfflineSigner { type Error: From; - // I really dislike existence of this function because it makes you re-derive your key **twice** for each contract transaction - fn signer_addresses(&self) -> Result, Self::Error> { - let derived_addresses = self - .get_accounts()? - .into_iter() - .map(|account| account.address) - .collect(); - Ok(derived_addresses) + fn signer_addresses(&self) -> Vec { + self.get_accounts() + .iter() + .map(|account| account.address.clone()) + .collect() } - fn get_accounts(&self) -> Result, Self::Error>; + fn get_accounts(&self) -> &[AccountData]; - fn find_account(&self, signer_address: &AccountId) -> Result { - // TODO: we could really use some zeroize action here - let accounts = self.get_accounts()?; - accounts - .into_iter() + fn find_account(&self, signer_address: &AccountId) -> Result<&AccountData, Self::Error> { + self.get_accounts() + .iter() .find(|account| &account.address == signer_address) .ok_or_else(|| { SigningError::AccountNotFound { @@ -76,7 +71,7 @@ pub trait OfflineSigner { message: M, ) -> Result { let signer = self.find_account(signer_address)?; - self.sign_raw_with_account(&signer, message) + self.sign_raw_with_account(signer, message) } fn sign_direct( @@ -85,7 +80,7 @@ pub trait OfflineSigner { sign_doc: SignDoc, ) -> Result { let signer = self.find_account(signer_address)?; - self.sign_direct_with_account(&signer, sign_doc) + self.sign_direct_with_account(signer, sign_doc) } // unless explicitly defined, each signing method is unsupported @@ -122,7 +117,7 @@ pub struct NoSigner; // impl OfflineSigner for NoSigner { // type Error = SignerUnavailable; // -// fn get_accounts(&self) -> Result, Self::Error> { +// fn get_accounts(&self) -> &[AccountData] { // return Err(SignerUnavailable); // } // } diff --git a/common/client-libs/validator-client/src/signing/tx_signer.rs b/common/client-libs/validator-client/src/signing/tx_signer.rs index f04047c39e..59289bbe09 100644 --- a/common/client-libs/validator-client/src/signing/tx_signer.rs +++ b/common/client-libs/validator-client/src/signing/tx_signer.rs @@ -50,7 +50,7 @@ pub trait TxSigner: OfflineSigner { ) .map_err(|source| SigningError::SignDocFailure { source })?; - self.sign_direct_with_account(&account_from_signer, sign_doc) + self.sign_direct_with_account(account_from_signer, sign_doc) } } diff --git a/common/commands/src/validator/account/create.rs b/common/commands/src/validator/account/create.rs index f2cd4eb336..0839ff1279 100644 --- a/common/commands/src/validator/account/create.rs +++ b/common/commands/src/validator/account/create.rs @@ -3,6 +3,7 @@ use clap::Parser; use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet; +use nym_validator_client::signing::signer::OfflineSigner; #[derive(Debug, Parser)] pub struct Args { @@ -15,9 +16,10 @@ pub fn create_account(args: Args, prefix: &str) { let word_count = args.word_count.unwrap_or(24); let mnemonic = bip39::Mnemonic::generate(word_count).expect("failed to generate mnemonic!"); - let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic); + let wallet = DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, mnemonic) + .expect("failed to derive accounts!"); // Output address and mnemonics into separate lines for easier parsing println!("{}", wallet.mnemonic_string().as_str()); - println!("{}", wallet.try_derive_accounts().unwrap()[0].address()); + println!("{}", wallet.signer_addresses()[0]); } diff --git a/common/commands/src/validator/account/pubkey.rs b/common/commands/src/validator/account/pubkey.rs index ed419c1101..fc9bf25f97 100644 --- a/common/commands/src/validator/account/pubkey.rs +++ b/common/commands/src/validator/account/pubkey.rs @@ -1,13 +1,13 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::context::QueryClient; +use crate::utils::show_error; use clap::Parser; use log::{error, info}; use nym_validator_client::nyxd::{AccountId, CosmWasmClient}; use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet; - -use crate::context::QueryClient; -use crate::utils::show_error; +use nym_validator_client::signing::signer::OfflineSigner; #[derive(Debug, Parser)] pub struct Args { @@ -50,20 +50,19 @@ pub async fn get_pubkey( } pub fn get_pubkey_from_mnemonic(address: AccountId, prefix: &str, mnemonic: bip39::Mnemonic) { - let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic); - match wallet.try_derive_accounts() { - Ok(accounts) => match accounts.iter().find(|a| *a.address() == address) { - Some(account) => { - println!("{}", account.public_key().to_string()); - } - None => { - error!("Could not derive key that matches {address}") - } - }, - Err(e) => { - error!("Failed to derive accounts. {e}"); + let wallet = match DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, mnemonic) { + Ok(wallet) => wallet, + Err(err) => { + error!("Failed to derive accounts. {err}"); + return; } - } + }; + + let Ok(account) = wallet.find_account(&address) else { + error!("Could not derive key that matches {address}"); + return; + }; + println!("{}", account.public_key().to_string()); } pub async fn get_pubkey_from_chain(address: AccountId, client: &QueryClient) { diff --git a/common/commands/src/validator/signature/sign.rs b/common/commands/src/validator/signature/sign.rs index 7df1d12eb6..6a2e3254b9 100644 --- a/common/commands/src/validator/signature/sign.rs +++ b/common/commands/src/validator/signature/sign.rs @@ -36,32 +36,35 @@ pub fn sign(args: Args, prefix: &str, mnemonic: Option) { return; } - let wallet = - DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic.expect("mnemonic not set")); - match wallet.try_derive_accounts() { - Ok(accounts) => match accounts.first() { - Some(account) => { - let msg = args.message.into_bytes(); - match wallet.sign_raw_with_account(account, msg) { - Ok(signature) => { - let output = SignatureOutputJson { - account_id: account.address().to_string(), - public_key: account.public_key(), - signature_as_hex: signature.to_string(), - }; - println!("{}", json!(output)); - } - Err(e) => { - error!("Failed to sign message. {e}"); - } + let wallet = match DirectSecp256k1HdWallet::checked_from_mnemonic( + prefix, + mnemonic.expect("mnemonic not set"), + ) { + Ok(wallet) => wallet, + Err(err) => { + error!("Could not derive an account key from the mnemonic: {err}"); + return; + } + }; + match wallet.get_accounts().first() { + Some(account) => { + let msg = args.message.into_bytes(); + match wallet.sign_raw_with_account(account, msg) { + Ok(signature) => { + let output = SignatureOutputJson { + account_id: account.address().to_string(), + public_key: account.public_key(), + signature_as_hex: signature.to_string(), + }; + println!("{}", json!(output)); + } + Err(e) => { + error!("Failed to sign message. {e}"); } } - None => { - error!("Could not derive an account key from the mnemonic",) - } - }, - Err(e) => { - error!("Failed to derive accounts. {e}"); + } + None => { + error!("Could not derive an account key from the mnemonic",) } } } diff --git a/common/types/src/error.rs b/common/types/src/error.rs index b72a4a18d6..e8d803b077 100644 --- a/common/types/src/error.rs +++ b/common/types/src/error.rs @@ -1,6 +1,7 @@ use nym_mixnet_contract_common::ContractsCommonError; use nym_validator_client::error::TendermintRpcError; use nym_validator_client::nym_api::error::NymAPIError; +use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWalletError; use nym_validator_client::{nyxd::error::NyxdError, ValidatorClientError}; use serde::{Serialize, Serializer}; use std::io; @@ -59,6 +60,8 @@ pub enum TypesError { #[from] source: cosmwasm_std::DecimalRangeExceeded, }, + #[error(transparent)] + AccountDerivationFailure(#[from] DirectSecp256k1HdWalletError), #[error("No nym API URL configured")] NoNymApiUrlConfigured, #[error("{0} is not a valid amount string")] @@ -113,6 +116,7 @@ impl From for TypesError { ValidatorClientError::InconsistentPagedMetadata => { TypesError::InconsistentPagedMetadata } + ValidatorClientError::AccountDerivationFailure { source } => source.into(), } } } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index 3319363be1..63dc75e884 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -11,7 +11,7 @@ use nym_config::defaults::{NymNetworkDetails, COSMOS_DERIVATION_PATH}; use nym_types::account::{Account, AccountEntry, Balance}; use nym_validator_client::nyxd::CosmWasmClient; use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet; -use nym_validator_client::signing::AccountData; +use nym_validator_client::signing::signer::OfflineSigner; use nym_validator_client::DirectSigningHttpRpcValidatorClient; use nym_wallet_types::network::Network as WalletNetwork; use std::collections::HashMap; @@ -575,10 +575,10 @@ fn derive_address( prefix: &str, ) -> Result { // note: the ephemeral wallet will zeroize the mnemonic on drop - DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic) - .try_derive_accounts()? + DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, mnemonic) + .map_err(|_| BackendError::FailedToDeriveAddress)? + .signer_addresses() .first() - .map(AccountData::address) .cloned() .ok_or(BackendError::FailedToDeriveAddress) } diff --git a/nym-wallet/src-tauri/src/operations/signatures/sign.rs b/nym-wallet/src-tauri/src/operations/signatures/sign.rs index e93edd57f4..16bb74ecef 100644 --- a/nym-wallet/src-tauri/src/operations/signatures/sign.rs +++ b/nym-wallet/src-tauri/src/operations/signatures/sign.rs @@ -24,7 +24,7 @@ pub async fn sign( ) -> Result { let guard = state.read().await; let client = guard.current_client()?; - let derived_accounts = client.nyxd.get_accounts()?; + let derived_accounts = client.nyxd.get_accounts(); let account = derived_accounts.first().ok_or_else(|| { log::error!(">>> Unable to derive account"); BackendError::SignatureError("unable to derive account".to_string()) diff --git a/tools/internal/testnet-manager/src/manager/contract.rs b/tools/internal/testnet-manager/src/manager/contract.rs index 5792dbd23e..4c04ade5d0 100644 --- a/tools/internal/testnet-manager/src/manager/contract.rs +++ b/tools/internal/testnet-manager/src/manager/contract.rs @@ -7,6 +7,7 @@ use nym_validator_client::nyxd::cosmwasm_client::types::{ ContractCodeId, InstantiateResult, MigrateResult, UploadResult, }; use nym_validator_client::nyxd::{AccountId, Hash}; +use nym_validator_client::signing::signer::OfflineSigner; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; @@ -145,12 +146,9 @@ impl Account { pub(crate) fn new() -> Account { let mnemonic = bip39::Mnemonic::generate(24).unwrap(); // sure, we're using hardcoded prefix, but realistically this will never change - let wallet = DirectSecp256k1HdWallet::from_mnemonic("n", mnemonic.clone()); - let acc = wallet.try_derive_accounts().unwrap().pop().unwrap(); - Account { - address: acc.address, - mnemonic, - } + let wallet = DirectSecp256k1HdWallet::checked_from_mnemonic("n", mnemonic.clone()).unwrap(); + let address = wallet.signer_addresses().pop().unwrap(); + Account { address, mnemonic } } pub(crate) fn address(&self) -> AccountId { From 4b35c362999a26ae80ec9eeb85209b3fcbbdb8a9 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Wed, 26 Nov 2025 12:05:48 +0100 Subject: [PATCH 171/180] add dev notes --- .../docs/pages/operators/changelog.mdx | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/documentation/docs/pages/operators/changelog.mdx b/documentation/docs/pages/operators/changelog.mdx index aa1fe30dfe..eda18da58d 100644 --- a/documentation/docs/pages/operators/changelog.mdx +++ b/documentation/docs/pages/operators/changelog.mdx @@ -49,6 +49,54 @@ This page displays a full list of all the changes during our release cycle from +## `v2025.21-mozzarella` + +- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.21-mozzarella) +- [`nym-node`](nodes/nym-node.mdx) version `1.22.0` + +```sh +some version output blabla +``` + +### Operators Updates & Tools + +- new cli +- new tunnel manager +- updated DP + + +### Features + +- [HTTP API resilience enable & domain rotation conditions](https://github.com/nymtech/nym/pull/6200): Changes to make sure fallback domains are updated / enabled only for relevant + +- [Typescript SDK 1.4.1](https://github.com/nymtech/nym/pull/6146): This PR is a new release of the Typescript SDK, `mixFetch` and `WASM` client. It also removes the Harbour Master client from `mixFetch`, replacing it with the Nym API's described endpoint for nym-nodes. + +- [Enable URL rotation and retries for mixnet gateway init](https://github.com/nymtech/nym/pull/6126): + - Multiple URL fallback with configurable retries (defaults to 3) + - Infallible URL conversion per feedback (`Url::from()` instead of `parse().ok()`) + - Non-breaking builder pattern for `BuilderConfig` per feedback + - Reverted redundant node filtering per clarification that API already filters by `supported_roles.entry` + +- [Credential proxy jwt](https://github.com/nymtech/nym/pull/5957): This PR is part of the 'Upgrade Mode' that should allow usage of the network in a situation where ecash credentials are unissuable, because, for example, we have lost signing quorum (i.e. we have fewer than the required number of threshold signers responding to requests). This version is more naive. Instead requesting actual 'emergency credentials' that would have been issued by a subset of ecash signers, the credentials proxy creates a JWT, signed with its key, attesting the upgrade mode has been enabled. + +### Bugfix + +- [Tunnel not waiting on `MixnetClient` to shut down cleanly](https://github.com/nymtech/nym/pull/6225): When there is a wireguard registration error, the mixnet client is signalled to shut down, but the tunnel doesn't wait. Now on errors, the registration client doesn't return until everything is properly stopped + +- [Fix credential proxy upgrade mode attestation url arg](https://github.com/nymtech/nym/pull/6202): This includes bringing over changes introduced in [\#6174](https://github.com/nymtech/nym/pull/6174) + +- [Remove debug feature from `http-macro` spec in gateway probe](https://github.com/nymtech/nym/pull/6195) + +- [DNS relibility and troubleshooting ](https://github.com/nymtech/nym/pull/6179) + +- [Distinguish authenticator errors by credential spent](https://github.com/nymtech/nym/pull/6176): This PR introduces a distinction between authenticator client errors that are happening before a credential was taken out of storage, and after ] + +- [Disconnect mixnet client if registration fails](https://github.com/nymtech/nym/pull/6158): Currently the registration client does its job and then handles everything to the `vpn-client`, who then takes care on the mixnet client. However, if the registration fails, nothing is disconnecting the mixnet client, which can lead to errors of the kind of `There is already an open connection to this client`. This PR addresses that + + +### Refactors & Maintenance + + ## `v2025.20-leerdammer` - [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.20-leerdammer) From fcf782674c3966baf428f0e4131dcb578abadd7c Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Wed, 26 Nov 2025 12:08:27 +0100 Subject: [PATCH 172/180] bump up version --- documentation/docs/pages/operators/changelog.mdx | 11 ++++++++++- .../docs/pages/operators/nodes/nym-node/setup.mdx | 8 ++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/documentation/docs/pages/operators/changelog.mdx b/documentation/docs/pages/operators/changelog.mdx index eda18da58d..2b26a5d19f 100644 --- a/documentation/docs/pages/operators/changelog.mdx +++ b/documentation/docs/pages/operators/changelog.mdx @@ -55,7 +55,16 @@ This page displays a full list of all the changes during our release cycle from - [`nym-node`](nodes/nym-node.mdx) version `1.22.0` ```sh -some version output blabla +nym-node +Binary Name: nym-node +Build Timestamp: 2025-11-25T14:26:29.627763948Z +Build Version: 1.22.0 +Commit SHA: 22793bc45ea21561671d6670497ff42bc36b9d76 +Commit Date: 2025-11-25T15:16:42.000000000+01:00 +Commit Branch: HEAD +rustc Version: 1.88.0 +rustc Channel: stable +cargo Profile: release ``` ### Operators Updates & Tools diff --git a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx index de584513ec..36e415e78a 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx @@ -21,10 +21,10 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](. ```sh nym-node Binary Name: nym-node -Build Timestamp: 2025-11-12T08:19:33.288341371Z -Build Version: 1.21.0 -Commit SHA: babf113fe5d396fa8a84fa939ad4b1b5b4d38b83 -Commit Date: 2025-11-12T08:39:48.000000000+01:00 +Build Timestamp: 2025-11-25T14:26:29.627763948Z +Build Version: 1.22.0 +Commit SHA: 22793bc45ea21561671d6670497ff42bc36b9d76 +Commit Date: 2025-11-25T15:16:42.000000000+01:00 Commit Branch: HEAD rustc Version: 1.88.0 rustc Channel: stable From 36d0adfe92810c821059b9805d322427dd3920a0 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Wed, 26 Nov 2025 12:15:50 +0100 Subject: [PATCH 173/180] add NTM info message --- documentation/docs/pages/operators/changelog.mdx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/documentation/docs/pages/operators/changelog.mdx b/documentation/docs/pages/operators/changelog.mdx index 2b26a5d19f..3d3ea3738e 100644 --- a/documentation/docs/pages/operators/changelog.mdx +++ b/documentation/docs/pages/operators/changelog.mdx @@ -69,6 +69,14 @@ cargo Profile: release ### Operators Updates & Tools + +**We are introducing new `network-tunnel-manager.sh` (NTM), with improved server configuration (see all changes here: [\#6186](https://github.com/nymtech/nym/pull/6186/)). We ask operators to re-run their routing configuration using the new NTM, following [these steps](/operators/nodes/nym-node/configuration#routing-configuration).** + +**PLEASE NOTE THAT THIS IS A NEW TOOL HANDLING VARIOUS COMPLEX SERVER SETTINGS, PLEASE RUN IT ON A FEW NODES AT A TIME, DO THE TESTING, MONITOR YOUR NODES AND REPORT ANY ISSUES** + +We will keep `nym-node v1.21.1` as the latest version on API side for another 3 days to not penalize operators taking time for a proper implementation. + + - new cli - new tunnel manager - updated DP From fd1b52403728617b18f2a1dfba5c8ed1c59421e3 Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Wed, 26 Nov 2025 13:13:37 +0100 Subject: [PATCH 174/180] add operators news --- .../docs/pages/operators/changelog.mdx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/documentation/docs/pages/operators/changelog.mdx b/documentation/docs/pages/operators/changelog.mdx index 3d3ea3738e..4036fc5aae 100644 --- a/documentation/docs/pages/operators/changelog.mdx +++ b/documentation/docs/pages/operators/changelog.mdx @@ -72,15 +72,18 @@ cargo Profile: release **We are introducing new `network-tunnel-manager.sh` (NTM), with improved server configuration (see all changes here: [\#6186](https://github.com/nymtech/nym/pull/6186/)). We ask operators to re-run their routing configuration using the new NTM, following [these steps](/operators/nodes/nym-node/configuration#routing-configuration).** -**PLEASE NOTE THAT THIS IS A NEW TOOL HANDLING VARIOUS COMPLEX SERVER SETTINGS, PLEASE RUN IT ON A FEW NODES AT A TIME, DO THE TESTING, MONITOR YOUR NODES AND REPORT ANY ISSUES** +**NOTE THAT THIS IS A NEW TOOL HANDLING VARIOUS COMPLEX SERVER SETTINGS, PLEASE RUN IT ON A FEW NODES AT A TIME, DO THE TESTING, MONITOR YOUR NODES AND REPORT ANY ISSUES** We will keep `nym-node v1.21.1` as the latest version on API side for another 3 days to not penalize operators taking time for a proper implementation. -- new cli -- new tunnel manager -- updated DP +We are proud to announce that there are several improvements on tools aiming to make node operators life easier. Please let us know how do these programs work for you. +- [**New Nym Node CLI version**](/operators/tools#nym-node-cli): This version introduces arguments, allowing the operator to pass all needed variables (ie. hostname, mode etcetra) and let the program setup everything needed for `nym-node`, server configuration, nginx, routing, tunnels and bridges without further prompts for these values. This version also installs [QUIC bridge](/operators/nodes/nym-node/configuration#quic-transport-bridge-deployment) + +- [**New Network Tunnel Manager**](/operators/nodes/nym-node/configuration#routing-configuration): NTM went through an big overhaul ([\#6186](https://github.com/nymtech/nym/pull/6186/)) - this new version combines old NTM with WG scripts. If run with `complete_networking_configuration` the tool does entire routing configuration, creates interfaces, and configures WireGuard exit policy. + +- [**Updated QUIC Deployment Tool**](/operators/nodes/nym-node/configuration#quic-transport-bridge-deployment): The program is lighter, stripped of redundant functions reworking iptables rules. ### Features @@ -104,14 +107,11 @@ We will keep `nym-node v1.21.1` as the latest version on API side for another 3 - [Remove debug feature from `http-macro` spec in gateway probe](https://github.com/nymtech/nym/pull/6195) -- [DNS relibility and troubleshooting ](https://github.com/nymtech/nym/pull/6179) +- [DNS reliability and troubleshooting ](https://github.com/nymtech/nym/pull/6179) -- [Distinguish authenticator errors by credential spent](https://github.com/nymtech/nym/pull/6176): This PR introduces a distinction between authenticator client errors that are happening before a credential was taken out of storage, and after ] +- [Distinguish authenticator errors by credential spent](https://github.com/nymtech/nym/pull/6176): This PR introduces a distinction between authenticator client errors that are happening before a credential was taken out of storage, and after - [Disconnect mixnet client if registration fails](https://github.com/nymtech/nym/pull/6158): Currently the registration client does its job and then handles everything to the `vpn-client`, who then takes care on the mixnet client. However, if the registration fails, nothing is disconnecting the mixnet client, which can lead to errors of the kind of `There is already an open connection to this client`. This PR addresses that - - -### Refactors & Maintenance ## `v2025.20-leerdammer` From 32e06e19e790bcb5111e2741d2ac30b1afd79bfd Mon Sep 17 00:00:00 2001 From: serinko <97586125+serinko@users.noreply.github.com> Date: Wed, 26 Nov 2025 13:18:47 +0100 Subject: [PATCH 175/180] bump stats --- .../docs/components/outputs/api-scraping-outputs/time-now.md | 2 +- documentation/docs/pages/operators/changelog.mdx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md index da40fc5444..a7fdf68bde 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md @@ -1 +1 @@ -Tuesday, November 25th 2025, 08:49:55 UTC +Wednesday, November 26th 2025, 12:13:56 UTC diff --git a/documentation/docs/pages/operators/changelog.mdx b/documentation/docs/pages/operators/changelog.mdx index 4036fc5aae..34c4551975 100644 --- a/documentation/docs/pages/operators/changelog.mdx +++ b/documentation/docs/pages/operators/changelog.mdx @@ -72,14 +72,14 @@ cargo Profile: release **We are introducing new `network-tunnel-manager.sh` (NTM), with improved server configuration (see all changes here: [\#6186](https://github.com/nymtech/nym/pull/6186/)). We ask operators to re-run their routing configuration using the new NTM, following [these steps](/operators/nodes/nym-node/configuration#routing-configuration).** -**NOTE THAT THIS IS A NEW TOOL HANDLING VARIOUS COMPLEX SERVER SETTINGS, PLEASE RUN IT ON A FEW NODES AT A TIME, DO THE TESTING, MONITOR YOUR NODES AND REPORT ANY ISSUES** +**NOTE THAT THIS IS A NEW TOOL HANDLING VARIOUS COMPLEX SERVER SETTINGS, PLEASE RUN IT ON A FEW NODES AT A TIME, DO THE TESTING, MONITOR YOUR NODES AND [REPORT ANY ISSUES](https://matrix.to/#/#operators:nymtech.chat).** We will keep `nym-node v1.21.1` as the latest version on API side for another 3 days to not penalize operators taking time for a proper implementation. We are proud to announce that there are several improvements on tools aiming to make node operators life easier. Please let us know how do these programs work for you. -- [**New Nym Node CLI version**](/operators/tools#nym-node-cli): This version introduces arguments, allowing the operator to pass all needed variables (ie. hostname, mode etcetra) and let the program setup everything needed for `nym-node`, server configuration, nginx, routing, tunnels and bridges without further prompts for these values. This version also installs [QUIC bridge](/operators/nodes/nym-node/configuration#quic-transport-bridge-deployment) +- [**New Nym Node CLI version**](/operators/tools#nym-node-cli): This version introduces arguments, allowing the operator to pass all needed variables (ie. hostname, mode etcetra) and let the program setup everything required for `nym-node` installation, server configuration, nginx, routing, tunnels and bridges, without further prompts for these values. This version also installs [QUIC bridge](/operators/nodes/nym-node/configuration#quic-transport-bridge-deployment). - [**New Network Tunnel Manager**](/operators/nodes/nym-node/configuration#routing-configuration): NTM went through an big overhaul ([\#6186](https://github.com/nymtech/nym/pull/6186/)) - this new version combines old NTM with WG scripts. If run with `complete_networking_configuration` the tool does entire routing configuration, creates interfaces, and configures WireGuard exit policy. From f5d22a66f6e877d7810d79e25093b8ad0e7cb8c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 27 Nov 2025 10:28:53 +0000 Subject: [PATCH 176/180] bugfix: reexposed 'derive_extended_private_key' (#6247) --- .../src/signing/direct_wallet.rs | 43 ++++++++++++++++++- .../validator-client/src/signing/mod.rs | 4 +- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/common/client-libs/validator-client/src/signing/direct_wallet.rs b/common/client-libs/validator-client/src/signing/direct_wallet.rs index 6192ead995..2cc64f8bfa 100644 --- a/common/client-libs/validator-client/src/signing/direct_wallet.rs +++ b/common/client-libs/validator-client/src/signing/direct_wallet.rs @@ -2,11 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 use crate::signing::signer::{OfflineSigner, SigningError}; -use crate::signing::{AccountData, Secp256k1Derivation}; +use crate::signing::{ + derive_extended_private_key, derive_keypair, AccountData, Secp256k1Derivation, Secp256k1Keypair, +}; +use bip32::XPrv; use cosmrs::bip32::DerivationPath; use cosmrs::tx; use cosmrs::tx::SignDoc; use nym_config::defaults; +use std::borrow::Cow; use thiserror::Error; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; @@ -104,6 +108,43 @@ impl DirectSecp256k1HdWallet { pub fn mnemonic_string(&self) -> Zeroizing { Zeroizing::new(self.secret.to_string()) } + + pub fn account_seed<'a, P: Into>>( + &self, + bip39_password: P, + ) -> Zeroizing<[u8; 64]> { + Zeroizing::new(self.secret.to_seed(bip39_password)) + } + + /// Derive an extended private key from the stored account secret assuming no bip39 password + #[deprecated( + note = "use derive_extended_private_key_with_password to ensure correct derivation if used bip39 password" + )] + pub fn derive_extended_private_key( + &self, + hd_path: &DerivationPath, + ) -> Result { + let seed = self.account_seed(""); + derive_extended_private_key(seed, hd_path) + } + + pub fn derive_keypair<'a, P: Into>>( + &self, + hd_path: &DerivationPath, + bip39_password: P, + ) -> Result { + let seed = self.account_seed(bip39_password); + derive_keypair(seed, hd_path) + } + + pub fn derive_extended_private_key_with_password<'a, P: Into>>( + &self, + hd_path: &DerivationPath, + bip39_password: P, + ) -> Result { + let seed = self.account_seed(bip39_password); + derive_extended_private_key(seed, hd_path) + } } #[must_use] diff --git a/common/client-libs/validator-client/src/signing/mod.rs b/common/client-libs/validator-client/src/signing/mod.rs index e3f6174115..ad5ac9f7e5 100644 --- a/common/client-libs/validator-client/src/signing/mod.rs +++ b/common/client-libs/validator-client/src/signing/mod.rs @@ -47,7 +47,7 @@ impl Secp256k1Derivation { } } -fn derive_keypair( +pub fn derive_keypair( seed: S, hd_path: &DerivationPath, ) -> Result @@ -62,7 +62,7 @@ where Ok((private_key, public_key)) } -fn derive_extended_private_key( +pub fn derive_extended_private_key( seed: S, hd_path: &DerivationPath, ) -> Result From 0b58b6f728238baba0aa1596c9a330dbeaf2afd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 28 Nov 2025 13:16:36 +0000 Subject: [PATCH 177/180] remove run DKG migration (#6253) --- Cargo.lock | 2 +- contracts/coconut-dkg/src/contract.rs | 4 +--- .../coconut-dkg/src/queued_migrations.rs | 19 ------------------- 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eeed1ac0e6..62d01b3e6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6561,7 +6561,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "4.0.11-testing" +version = "4.0.12" dependencies = [ "ammonia", "anyhow", diff --git a/contracts/coconut-dkg/src/contract.rs b/contracts/coconut-dkg/src/contract.rs index d5d525dd4c..6f3e331ed3 100644 --- a/contracts/coconut-dkg/src/contract.rs +++ b/contracts/coconut-dkg/src/contract.rs @@ -255,12 +255,10 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result, env: Env, _msg: MigrateMsg) -> Result { +pub fn migrate(deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result { set_build_information!(deps.storage)?; cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - crate::queued_migrations::introduce_historical_epochs(deps, env)?; - Ok(Response::new()) } diff --git a/contracts/coconut-dkg/src/queued_migrations.rs b/contracts/coconut-dkg/src/queued_migrations.rs index 54c149a518..7e1e3cacd6 100644 --- a/contracts/coconut-dkg/src/queued_migrations.rs +++ b/contracts/coconut-dkg/src/queued_migrations.rs @@ -1,21 +1,2 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 - -use crate::epoch_state::storage::HISTORICAL_EPOCH; -use crate::error::ContractError; -use cosmwasm_std::{DepsMut, Env}; - -pub fn introduce_historical_epochs(deps: DepsMut, env: Env) -> Result<(), ContractError> { - if HISTORICAL_EPOCH.may_load(deps.storage)?.is_some() { - return Err(ContractError::FailedMigration { - comment: "this migration has already been run before".to_string(), - }); - } - - #[allow(deprecated)] - let current = crate::epoch_state::storage::CURRENT_EPOCH.load(deps.storage)?; - // we won't have information on intermediate states prior to now, but that's not the end of the world - HISTORICAL_EPOCH.save(deps.storage, ¤t, env.block.height)?; - - Ok(()) -} From 37ae72d8ec7165478b8bc7b8c5bd62ab7fb456c7 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Fri, 28 Nov 2025 19:18:05 +0100 Subject: [PATCH 178/180] bump versions --- Cargo.lock | 14 +++++++------- clients/native/Cargo.toml | 2 +- clients/socks5/Cargo.toml | 2 +- nym-api/Cargo.toml | 2 +- nym-node/Cargo.toml | 2 +- service-providers/network-requester/Cargo.toml | 2 +- tools/nym-cli/Cargo.toml | 2 +- tools/nymvisor/Cargo.toml | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62d01b3e6d..3a9d8e0676 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4834,7 +4834,7 @@ dependencies = [ [[package]] name = "nym-api" -version = "1.1.70" +version = "1.1.71" dependencies = [ "anyhow", "async-trait", @@ -5057,7 +5057,7 @@ dependencies = [ [[package]] name = "nym-cli" -version = "1.1.67" +version = "1.1.68" dependencies = [ "anyhow", "base64 0.22.1", @@ -5140,7 +5140,7 @@ dependencies = [ [[package]] name = "nym-client" -version = "1.1.67" +version = "1.1.68" dependencies = [ "bs58", "clap", @@ -6367,7 +6367,7 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.1.68" +version = "1.1.69" dependencies = [ "addr", "anyhow", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.22.0" +version = "1.23.0" dependencies = [ "anyhow", "arc-swap", @@ -6946,7 +6946,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.67" +version = "1.1.68" dependencies = [ "bs58", "clap", @@ -7680,7 +7680,7 @@ dependencies = [ [[package]] name = "nymvisor" -version = "0.1.32" +version = "0.1.33" dependencies = [ "anyhow", "bytes", diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index e53458609b..026f483626 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-client" -version = "1.1.67" +version = "1.1.68" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 91d6591411..500dd6cc6a 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-socks5-client" -version = "1.1.67" +version = "1.1.68" authors = ["Dave Hrycyszyn "] description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address" edition = "2021" diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 362c78f731..c7037d32f9 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-api" license = "GPL-3.0" -version = "1.1.70" +version = "1.1.71" authors.workspace = true edition = "2021" rust-version.workspace = true diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 3e56e037d3..f8467cdd3c 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.22.0" +version = "1.23.0" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index 1731d38bef..2c11e6ea3f 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-network-requester" license = "GPL-3.0" -version = "1.1.68" +version = "1.1.69" authors.workspace = true edition.workspace = true rust-version = "1.85" diff --git a/tools/nym-cli/Cargo.toml b/tools/nym-cli/Cargo.toml index 79edd2de27..1a8aa016eb 100644 --- a/tools/nym-cli/Cargo.toml +++ b/tools/nym-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-cli" -version = "1.1.67" +version = "1.1.68" authors.workspace = true edition = "2021" license.workspace = true diff --git a/tools/nymvisor/Cargo.toml b/tools/nymvisor/Cargo.toml index 738a50a88e..7329119eb9 100644 --- a/tools/nymvisor/Cargo.toml +++ b/tools/nymvisor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nymvisor" -version = "0.1.32" +version = "0.1.33" authors.workspace = true repository.workspace = true homepage.workspace = true From 46fe1bc8191f42aa27f34743c96e9e9f26453d87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 2 Dec 2025 15:29:30 +0000 Subject: [PATCH 179/180] bugfix: mozzarella -> niolo config migration (#6259) * bugfix: mozzarella -> niolo config migration * clippy --- nym-node/src/config/old_configs/mod.rs | 2 + .../src/config/old_configs/old_config_v10.rs | 424 +------------ .../src/config/old_configs/old_config_v11.rs | 599 ++++++++++++++++++ nym-node/src/config/upgrade_helpers.rs | 3 +- 4 files changed, 627 insertions(+), 401 deletions(-) create mode 100644 nym-node/src/config/old_configs/old_config_v11.rs diff --git a/nym-node/src/config/old_configs/mod.rs b/nym-node/src/config/old_configs/mod.rs index f87c88a43a..c46a72d1a1 100644 --- a/nym-node/src/config/old_configs/mod.rs +++ b/nym-node/src/config/old_configs/mod.rs @@ -3,6 +3,7 @@ mod old_config_v1; mod old_config_v10; +mod old_config_v11; mod old_config_v2; mod old_config_v3; mod old_config_v4; @@ -22,3 +23,4 @@ pub use old_config_v7::try_upgrade_config_v7; pub use old_config_v8::try_upgrade_config_v8; pub use old_config_v9::try_upgrade_config_v9; pub use old_config_v10::try_upgrade_config_v10; +pub use old_config_v11::try_upgrade_config_v11; diff --git a/nym-node/src/config/old_configs/old_config_v10.rs b/nym-node/src/config/old_configs/old_config_v10.rs index e45cca8dd2..bda32f2938 100644 --- a/nym-node/src/config/old_configs/old_config_v10.rs +++ b/nym-node/src/config/old_configs/old_config_v10.rs @@ -1,26 +1,11 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::config::authenticator::{Authenticator, AuthenticatorDebug}; -use crate::config::gateway_tasks::{ - ClientBandwidthDebug, StaleMessageDebug, UpgradeModeWatcher, ZkNymTicketHandlerDebug, -}; -use crate::config::persistence::{ - AuthenticatorPaths, GatewayTasksPaths, IpPacketRouterPaths, KeysPaths, NetworkRequesterPaths, - NymNodePaths, ReplayProtectionPaths, ServiceProvidersPaths, WireguardPaths, -}; -use crate::config::service_providers::{ - IpPacketRouter, IpPacketRouterDebug, NetworkRequester, NetworkRequesterDebug, -}; -use crate::config::{ - Config, DEFAULT_HTTP_PORT, GatewayTasksConfig, Host, Http, KeyRotation, KeyRotationDebug, - Mixnet, MixnetDebug, NodeModes, ReplayProtection, ReplayProtectionDebug, - ServiceProvidersConfig, Verloc, VerlocDebug, Wireguard, gateway_tasks, service_providers, -}; +use crate::config::old_configs::old_config_v11::{ConfigV11, WireguardV11}; +use crate::config::{DEFAULT_HTTP_PORT, NodeModes}; use crate::error::NymNodeError; use celes::Country; use clap::ValueEnum; -use nym_bin_common::logging::LoggingSettings; use nym_client_core_config_types::DebugConfig as ClientDebugConfig; use nym_config::defaults::{DEFAULT_VERLOC_LISTENING_PORT, WG_METADATA_PORT}; use nym_config::helpers::{in6addr_any_init, inaddr_any}; @@ -33,7 +18,7 @@ use serde::{Deserialize, Serialize}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::path::{Path, PathBuf}; use std::time::Duration; -use tracing::{debug, error, instrument}; +use tracing::{debug, instrument}; use url::Url; #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] @@ -1169,7 +1154,7 @@ impl ConfigV10 { pub async fn try_upgrade_config_v10>( path: P, prev_config: Option, -) -> Result { +) -> Result { debug!("attempting to load v10 config..."); let old_cfg = if let Some(prev_config) = prev_config { @@ -1178,398 +1163,37 @@ pub async fn try_upgrade_config_v10>( ConfigV10::read_from_path(&path)? }; - let cfg = Config { + let cfg = ConfigV11 { save_path: old_cfg.save_path, id: old_cfg.id, - modes: NodeModes { - mixnode: old_cfg.modes.mixnode, - entry: old_cfg.modes.entry, - exit: old_cfg.modes.exit, - }, - host: Host { - public_ips: old_cfg.host.public_ips, - hostname: old_cfg.host.hostname, - location: old_cfg.host.location, - }, - mixnet: Mixnet { - bind_address: old_cfg.mixnet.bind_address, - announce_port: old_cfg.mixnet.announce_port, - nym_api_urls: old_cfg.mixnet.nym_api_urls, - nyxd_urls: old_cfg.mixnet.nyxd_urls, - replay_protection: ReplayProtection { - storage_paths: ReplayProtectionPaths { - current_bloomfilters_directory: old_cfg - .mixnet - .replay_protection - .storage_paths - .current_bloomfilters_directory, - }, - debug: ReplayProtectionDebug { - unsafe_disabled: old_cfg.mixnet.replay_protection.debug.unsafe_disabled, - maximum_replay_detection_deferral: old_cfg - .mixnet - .replay_protection - .debug - .maximum_replay_detection_deferral, - maximum_replay_detection_pending_packets: old_cfg - .mixnet - .replay_protection - .debug - .maximum_replay_detection_pending_packets, - false_positive_rate: old_cfg.mixnet.replay_protection.debug.false_positive_rate, - initial_expected_packets_per_second: old_cfg - .mixnet - .replay_protection - .debug - .initial_expected_packets_per_second, - bloomfilter_minimum_packets_per_second_size: old_cfg - .mixnet - .replay_protection - .debug - .bloomfilter_minimum_packets_per_second_size, - bloomfilter_size_multiplier: old_cfg - .mixnet - .replay_protection - .debug - .bloomfilter_size_multiplier, - bloomfilter_disk_flushing_rate: old_cfg - .mixnet - .replay_protection - .debug - .bloomfilter_disk_flushing_rate, - }, - }, - key_rotation: KeyRotation { - debug: KeyRotationDebug { - rotation_state_poling_interval: old_cfg - .mixnet - .key_rotation - .debug - .rotation_state_poling_interval, - }, - }, - debug: MixnetDebug { - maximum_forward_packet_delay: old_cfg.mixnet.debug.maximum_forward_packet_delay, - packet_forwarding_initial_backoff: old_cfg - .mixnet - .debug - .packet_forwarding_initial_backoff, - packet_forwarding_maximum_backoff: old_cfg - .mixnet - .debug - .packet_forwarding_maximum_backoff, - initial_connection_timeout: old_cfg.mixnet.debug.initial_connection_timeout, - maximum_connection_buffer_size: old_cfg.mixnet.debug.maximum_connection_buffer_size, - unsafe_disable_noise: old_cfg.mixnet.debug.unsafe_disable_noise, - use_legacy_packet_encoding: old_cfg.mixnet.debug.use_legacy_packet_encoding, - }, - }, - storage_paths: NymNodePaths { - keys: KeysPaths { - private_ed25519_identity_key_file: old_cfg - .storage_paths - .keys - .private_ed25519_identity_key_file, - public_ed25519_identity_key_file: old_cfg - .storage_paths - .keys - .public_ed25519_identity_key_file, - primary_x25519_sphinx_key_file: old_cfg - .storage_paths - .keys - .primary_x25519_sphinx_key_file, - private_x25519_noise_key_file: old_cfg - .storage_paths - .keys - .private_x25519_noise_key_file, - public_x25519_noise_key_file: old_cfg - .storage_paths - .keys - .public_x25519_noise_key_file, - secondary_x25519_sphinx_key_file: old_cfg - .storage_paths - .keys - .secondary_x25519_sphinx_key_file, - }, - description: old_cfg.storage_paths.description, - }, - http: Http { - bind_address: old_cfg.http.bind_address, - landing_page_assets_path: old_cfg.http.landing_page_assets_path, - access_token: old_cfg.http.access_token, - expose_system_info: old_cfg.http.expose_system_info, - expose_system_hardware: old_cfg.http.expose_system_hardware, - expose_crypto_hardware: old_cfg.http.expose_crypto_hardware, - node_load_cache_ttl: old_cfg.http.node_load_cache_ttl, - }, - verloc: Verloc { - bind_address: old_cfg.verloc.bind_address, - announce_port: old_cfg.verloc.announce_port, - debug: VerlocDebug { - packets_per_node: old_cfg.verloc.debug.packets_per_node, - connection_timeout: old_cfg.verloc.debug.connection_timeout, - packet_timeout: old_cfg.verloc.debug.packet_timeout, - delay_between_packets: old_cfg.verloc.debug.delay_between_packets, - tested_nodes_batch_size: old_cfg.verloc.debug.tested_nodes_batch_size, - testing_interval: old_cfg.verloc.debug.testing_interval, - retry_timeout: old_cfg.verloc.debug.retry_timeout, - }, - }, - wireguard: Wireguard { + modes: old_cfg.modes, + host: old_cfg.host, + mixnet: old_cfg.mixnet, + storage_paths: old_cfg.storage_paths, + http: old_cfg.http, + verloc: old_cfg.verloc, + wireguard: WireguardV11 { enabled: old_cfg.wireguard.enabled, bind_address: old_cfg.wireguard.bind_address, private_ipv4: old_cfg.wireguard.private_ipv4, private_ipv6: old_cfg.wireguard.private_ipv6, + + // \/ CHANGED announced_tunnel_port: old_cfg.wireguard.announced_port, + // /\ CHANGED + + // \/ ADDED announced_metadata_port: WG_METADATA_PORT, + // /\ ADDED private_network_prefix_v4: old_cfg.wireguard.private_network_prefix_v4, private_network_prefix_v6: old_cfg.wireguard.private_network_prefix_v6, - storage_paths: WireguardPaths { - private_diffie_hellman_key_file: old_cfg - .wireguard - .storage_paths - .private_diffie_hellman_key_file, - public_diffie_hellman_key_file: old_cfg - .wireguard - .storage_paths - .public_diffie_hellman_key_file, - }, + storage_paths: old_cfg.wireguard.storage_paths, }, - gateway_tasks: GatewayTasksConfig { - storage_paths: GatewayTasksPaths { - clients_storage: old_cfg.gateway_tasks.storage_paths.clients_storage, - stats_storage: old_cfg.gateway_tasks.storage_paths.stats_storage, - cosmos_mnemonic: old_cfg.gateway_tasks.storage_paths.cosmos_mnemonic, - bridge_client_params: old_cfg.gateway_tasks.storage_paths.bridge_client_params, - }, - enforce_zk_nyms: old_cfg.gateway_tasks.enforce_zk_nyms, - ws_bind_address: old_cfg.gateway_tasks.ws_bind_address, - announce_ws_port: old_cfg.gateway_tasks.announce_ws_port, - announce_wss_port: old_cfg.gateway_tasks.announce_wss_port, - upgrade_mode: UpgradeModeWatcher::new() - .inspect_err(|_| { - error!( - "failed to set custom upgrade mode configuration - falling back to mainnet" - ) - }) - .unwrap_or(UpgradeModeWatcher::new_mainnet()), - debug: gateway_tasks::Debug { - message_retrieval_limit: old_cfg.gateway_tasks.debug.message_retrieval_limit, - maximum_open_connections: old_cfg.gateway_tasks.debug.maximum_open_connections, - minimum_mix_performance: old_cfg.gateway_tasks.debug.minimum_mix_performance, - max_request_timestamp_skew: old_cfg.gateway_tasks.debug.max_request_timestamp_skew, - stale_messages: StaleMessageDebug { - cleaner_run_interval: old_cfg - .gateway_tasks - .debug - .stale_messages - .cleaner_run_interval, - max_age: old_cfg.gateway_tasks.debug.stale_messages.max_age, - }, - client_bandwidth: ClientBandwidthDebug { - max_flushing_rate: old_cfg - .gateway_tasks - .debug - .client_bandwidth - .max_flushing_rate, - max_delta_flushing_amount: old_cfg - .gateway_tasks - .debug - .client_bandwidth - .max_delta_flushing_amount, - }, - zk_nym_tickets: ZkNymTicketHandlerDebug { - revocation_bandwidth_penalty: old_cfg - .gateway_tasks - .debug - .zk_nym_tickets - .revocation_bandwidth_penalty, - pending_poller: old_cfg.gateway_tasks.debug.zk_nym_tickets.pending_poller, - minimum_api_quorum: old_cfg - .gateway_tasks - .debug - .zk_nym_tickets - .minimum_api_quorum, - minimum_redemption_tickets: old_cfg - .gateway_tasks - .debug - .zk_nym_tickets - .minimum_redemption_tickets, - maximum_time_between_redemption: old_cfg - .gateway_tasks - .debug - .zk_nym_tickets - .maximum_time_between_redemption, - }, - ..Default::default() - }, - }, - service_providers: ServiceProvidersConfig { - storage_paths: ServiceProvidersPaths { - clients_storage: old_cfg.service_providers.storage_paths.clients_storage, - stats_storage: old_cfg.service_providers.storage_paths.stats_storage, - network_requester: NetworkRequesterPaths { - private_ed25519_identity_key_file: old_cfg - .service_providers - .storage_paths - .network_requester - .private_ed25519_identity_key_file, - public_ed25519_identity_key_file: old_cfg - .service_providers - .storage_paths - .network_requester - .public_ed25519_identity_key_file, - private_x25519_diffie_hellman_key_file: old_cfg - .service_providers - .storage_paths - .network_requester - .private_x25519_diffie_hellman_key_file, - public_x25519_diffie_hellman_key_file: old_cfg - .service_providers - .storage_paths - .network_requester - .public_x25519_diffie_hellman_key_file, - ack_key_file: old_cfg - .service_providers - .storage_paths - .network_requester - .ack_key_file, - reply_surb_database: old_cfg - .service_providers - .storage_paths - .network_requester - .reply_surb_database, - gateway_registrations: old_cfg - .service_providers - .storage_paths - .network_requester - .gateway_registrations, - }, - ip_packet_router: IpPacketRouterPaths { - private_ed25519_identity_key_file: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .private_ed25519_identity_key_file, - public_ed25519_identity_key_file: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .public_ed25519_identity_key_file, - private_x25519_diffie_hellman_key_file: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .private_x25519_diffie_hellman_key_file, - public_x25519_diffie_hellman_key_file: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .public_x25519_diffie_hellman_key_file, - ack_key_file: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .ack_key_file, - reply_surb_database: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .reply_surb_database, - gateway_registrations: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .gateway_registrations, - }, - authenticator: AuthenticatorPaths { - private_ed25519_identity_key_file: old_cfg - .service_providers - .storage_paths - .authenticator - .private_ed25519_identity_key_file, - public_ed25519_identity_key_file: old_cfg - .service_providers - .storage_paths - .authenticator - .public_ed25519_identity_key_file, - private_x25519_diffie_hellman_key_file: old_cfg - .service_providers - .storage_paths - .authenticator - .private_x25519_diffie_hellman_key_file, - public_x25519_diffie_hellman_key_file: old_cfg - .service_providers - .storage_paths - .authenticator - .public_x25519_diffie_hellman_key_file, - ack_key_file: old_cfg - .service_providers - .storage_paths - .authenticator - .ack_key_file, - reply_surb_database: old_cfg - .service_providers - .storage_paths - .authenticator - .reply_surb_database, - gateway_registrations: old_cfg - .service_providers - .storage_paths - .authenticator - .gateway_registrations, - }, - }, - open_proxy: old_cfg.service_providers.open_proxy, - upstream_exit_policy_url: old_cfg.service_providers.upstream_exit_policy_url, - network_requester: NetworkRequester { - debug: NetworkRequesterDebug { - enabled: old_cfg.service_providers.network_requester.debug.enabled, - disable_poisson_rate: old_cfg - .service_providers - .network_requester - .debug - .disable_poisson_rate, - client_debug: old_cfg - .service_providers - .network_requester - .debug - .client_debug, - }, - }, - ip_packet_router: IpPacketRouter { - debug: IpPacketRouterDebug { - enabled: old_cfg.service_providers.ip_packet_router.debug.enabled, - disable_poisson_rate: old_cfg - .service_providers - .ip_packet_router - .debug - .disable_poisson_rate, - client_debug: old_cfg - .service_providers - .ip_packet_router - .debug - .client_debug, - }, - }, - authenticator: Authenticator { - debug: AuthenticatorDebug { - enabled: old_cfg.service_providers.authenticator.debug.enabled, - disable_poisson_rate: old_cfg - .service_providers - .authenticator - .debug - .disable_poisson_rate, - client_debug: old_cfg.service_providers.authenticator.debug.client_debug, - }, - }, - debug: service_providers::Debug { - message_retrieval_limit: old_cfg.service_providers.debug.message_retrieval_limit, - }, - }, - metrics: Default::default(), - logging: LoggingSettings {}, - debug: Default::default(), + gateway_tasks: old_cfg.gateway_tasks, + service_providers: old_cfg.service_providers, + metrics: old_cfg.metrics, + logging: old_cfg.logging, + debug: old_cfg.debug, }; Ok(cfg) } diff --git a/nym-node/src/config/old_configs/old_config_v11.rs b/nym-node/src/config/old_configs/old_config_v11.rs new file mode 100644 index 0000000000..a2ac70a122 --- /dev/null +++ b/nym-node/src/config/old_configs/old_config_v11.rs @@ -0,0 +1,599 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::authenticator::{Authenticator, AuthenticatorDebug}; +use crate::config::gateway_tasks::{ + ClientBandwidthDebug, StaleMessageDebug, UpgradeModeWatcher, ZkNymTicketHandlerDebug, +}; +use crate::config::persistence::{ + AuthenticatorPaths, GatewayTasksPaths, IpPacketRouterPaths, KeysPaths, NetworkRequesterPaths, + NymNodePaths, ReplayProtectionPaths, ServiceProvidersPaths, WireguardPaths, +}; +use crate::config::service_providers::{ + IpPacketRouter, IpPacketRouterDebug, NetworkRequester, NetworkRequesterDebug, +}; +use crate::config::{ + Config, GatewayTasksConfig, Host, Http, KeyRotation, KeyRotationDebug, Mixnet, MixnetDebug, + NodeModes, ReplayProtection, ReplayProtectionDebug, ServiceProvidersConfig, Verloc, + VerlocDebug, Wireguard, gateway_tasks, service_providers, +}; +use crate::error::NymNodeError; +use nym_bin_common::logging::LoggingSettings; +use nym_config::read_config_from_toml_file; +use serde::{Deserialize, Serialize}; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::path::{Path, PathBuf}; +use tracing::{debug, error, instrument}; + +pub use unchanged_v11_types::*; + +// (while some of those are technically unused, they might be needed in future migrations, +// thus allow them to exist) +#[allow(dead_code)] +pub mod unchanged_v11_types { + use crate::config::old_configs::old_config_v10::{ + AuthenticatorDebugV10, AuthenticatorPathsV10, AuthenticatorV10, ClientBandwidthDebugV10, + DebugV10, GatewayTasksConfigDebugV10, GatewayTasksConfigV10, GatewayTasksPathsV10, HostV10, + HttpV10, IpPacketRouterDebugV10, IpPacketRouterPathsV10, IpPacketRouterV10, + KeyRotationDebugV10, KeyRotationV10, KeysPathsV10, LoggingSettingsV10, MetricsConfigV10, + MetricsDebugV10, MixnetDebugV10, MixnetV10, NetworkRequesterDebugV10, + NetworkRequesterPathsV10, NetworkRequesterV10, NodeModeV10, NodeModesV10, NymNodePathsV10, + ReplayProtectionDebugV10, ReplayProtectionPathsV10, ReplayProtectionV10, + ServiceProvidersConfigDebugV10, ServiceProvidersConfigV10, ServiceProvidersPathsV10, + StaleMessageDebugV10, VerlocDebugV10, VerlocV10, WireguardPathsV10, + ZkNymTicketHandlerDebugV10, + }; + + pub type WireguardPathsV11 = WireguardPathsV10; + pub type NodeModeV11 = NodeModeV10; + pub type NodeModesV11 = NodeModesV10; + pub type HostV11 = HostV10; + pub type KeyRotationDebugV11 = KeyRotationDebugV10; + pub type KeyRotationV11 = KeyRotationV10; + pub type MixnetDebugV11 = MixnetDebugV10; + pub type MixnetV11 = MixnetV10; + pub type ReplayProtectionV11 = ReplayProtectionV10; + pub type ReplayProtectionPathsV11 = ReplayProtectionPathsV10; + pub type ReplayProtectionDebugV11 = ReplayProtectionDebugV10; + pub type KeysPathsV11 = KeysPathsV10; + pub type NymNodePathsV11 = NymNodePathsV10; + pub type HttpV11 = HttpV10; + pub type VerlocDebugV11 = VerlocDebugV10; + pub type VerlocV11 = VerlocV10; + pub type DebugV11 = DebugV10; + pub type ZkNymTicketHandlerDebugV11 = ZkNymTicketHandlerDebugV10; + pub type NetworkRequesterPathsV11 = NetworkRequesterPathsV10; + pub type IpPacketRouterPathsV11 = IpPacketRouterPathsV10; + pub type AuthenticatorPathsV11 = AuthenticatorPathsV10; + pub type AuthenticatorV11 = AuthenticatorV10; + pub type AuthenticatorDebugV11 = AuthenticatorDebugV10; + pub type IpPacketRouterDebugV11 = IpPacketRouterDebugV10; + pub type IpPacketRouterV11 = IpPacketRouterV10; + pub type NetworkRequesterDebugV11 = NetworkRequesterDebugV10; + pub type NetworkRequesterV11 = NetworkRequesterV10; + pub type GatewayTasksPathsV11 = GatewayTasksPathsV10; + pub type StaleMessageDebugV11 = StaleMessageDebugV10; + pub type ClientBandwidthDebugV11 = ClientBandwidthDebugV10; + pub type GatewayTasksConfigDebugV11 = GatewayTasksConfigDebugV10; + pub type GatewayTasksConfigV11 = GatewayTasksConfigV10; + pub type ServiceProvidersPathsV11 = ServiceProvidersPathsV10; + pub type ServiceProvidersConfigDebugV11 = ServiceProvidersConfigDebugV10; + pub type ServiceProvidersConfigV11 = ServiceProvidersConfigV10; + pub type MetricsConfigV11 = MetricsConfigV10; + pub type MetricsDebugV11 = MetricsDebugV10; + pub type LoggingSettingsV11 = LoggingSettingsV10; +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WireguardV11 { + /// Specifies whether the wireguard service is enabled on this node. + pub enabled: bool, + + /// Socket address this node will use for binding its wireguard interface. + /// default: `[::]:51822` + pub bind_address: SocketAddr, + + /// Private IPv4 address of the wireguard gateway. + /// default: `10.1.0.1` + pub private_ipv4: Ipv4Addr, + + /// Private IPv6 address of the wireguard gateway. + /// default: `fc01::1` + pub private_ipv6: Ipv6Addr, + + /// Tunnel port announced to external clients wishing to connect to the wireguard interface. + /// Useful in the instances where the node is behind a proxy. + pub announced_tunnel_port: u16, + + /// Metadata port announced to external clients wishing to connect to the metadata endpoint. + /// Useful in the instances where the node is behind a proxy. + pub announced_metadata_port: u16, + + /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. + /// The maximum value for IPv4 is 32 + pub private_network_prefix_v4: u8, + + /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv6. + /// The maximum value for IPv6 is 128 + pub private_network_prefix_v6: u8, + + /// Paths for wireguard keys, client registries, etc. + pub storage_paths: WireguardPathsV11, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV11 { + // additional metadata holding on-disk location of this config file + #[serde(skip)] + pub(crate) save_path: Option, + + /// Human-readable ID of this particular node. + pub id: String, + + /// Current modes of this nym-node. + pub modes: NodeModesV11, + + pub host: HostV11, + + pub mixnet: MixnetV11, + + /// Storage paths to persistent nym-node data, such as its long term keys. + pub storage_paths: NymNodePathsV11, + + #[serde(default)] + pub http: HttpV11, + + #[serde(default)] + pub verloc: VerlocV11, + + pub wireguard: WireguardV11, + + #[serde(alias = "entry_gateway")] + pub gateway_tasks: GatewayTasksConfigV11, + + #[serde(alias = "exit_gateway")] + pub service_providers: ServiceProvidersConfigV11, + + #[serde(default)] + pub metrics: MetricsConfigV11, + + #[serde(default)] + pub logging: LoggingSettingsV11, + + #[serde(default)] + pub debug: DebugV11, +} + +impl ConfigV11 { + // simple wrapper that reads config file and assigns path location + fn read_from_path>(path: P) -> Result { + let path = path.as_ref(); + let mut loaded: ConfigV11 = + read_config_from_toml_file(path).map_err(|source| NymNodeError::ConfigLoadFailure { + path: path.to_path_buf(), + source, + })?; + loaded.save_path = Some(path.to_path_buf()); + debug!("loaded config file from {}", path.display()); + Ok(loaded) + } +} + +#[instrument(skip_all)] +pub async fn try_upgrade_config_v11>( + path: P, + prev_config: Option, +) -> Result { + debug!("attempting to load v11 config..."); + + let old_cfg = if let Some(prev_config) = prev_config { + prev_config + } else { + ConfigV11::read_from_path(&path)? + }; + + // for future reference: when creating v12 migration, + // look at how v10 -> v11 is implemented + // you might be able to create a bunch of type aliases again to save you some headache + let cfg = Config { + save_path: old_cfg.save_path, + id: old_cfg.id, + modes: NodeModes { + mixnode: old_cfg.modes.mixnode, + entry: old_cfg.modes.entry, + exit: old_cfg.modes.exit, + }, + host: Host { + public_ips: old_cfg.host.public_ips, + hostname: old_cfg.host.hostname, + location: old_cfg.host.location, + }, + mixnet: Mixnet { + bind_address: old_cfg.mixnet.bind_address, + announce_port: old_cfg.mixnet.announce_port, + nym_api_urls: old_cfg.mixnet.nym_api_urls, + nyxd_urls: old_cfg.mixnet.nyxd_urls, + replay_protection: ReplayProtection { + storage_paths: ReplayProtectionPaths { + current_bloomfilters_directory: old_cfg + .mixnet + .replay_protection + .storage_paths + .current_bloomfilters_directory, + }, + debug: ReplayProtectionDebug { + unsafe_disabled: old_cfg.mixnet.replay_protection.debug.unsafe_disabled, + maximum_replay_detection_deferral: old_cfg + .mixnet + .replay_protection + .debug + .maximum_replay_detection_deferral, + maximum_replay_detection_pending_packets: old_cfg + .mixnet + .replay_protection + .debug + .maximum_replay_detection_pending_packets, + false_positive_rate: old_cfg.mixnet.replay_protection.debug.false_positive_rate, + initial_expected_packets_per_second: old_cfg + .mixnet + .replay_protection + .debug + .initial_expected_packets_per_second, + bloomfilter_minimum_packets_per_second_size: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_minimum_packets_per_second_size, + bloomfilter_size_multiplier: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_size_multiplier, + bloomfilter_disk_flushing_rate: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_disk_flushing_rate, + }, + }, + key_rotation: KeyRotation { + debug: KeyRotationDebug { + rotation_state_poling_interval: old_cfg + .mixnet + .key_rotation + .debug + .rotation_state_poling_interval, + }, + }, + debug: MixnetDebug { + maximum_forward_packet_delay: old_cfg.mixnet.debug.maximum_forward_packet_delay, + packet_forwarding_initial_backoff: old_cfg + .mixnet + .debug + .packet_forwarding_initial_backoff, + packet_forwarding_maximum_backoff: old_cfg + .mixnet + .debug + .packet_forwarding_maximum_backoff, + initial_connection_timeout: old_cfg.mixnet.debug.initial_connection_timeout, + maximum_connection_buffer_size: old_cfg.mixnet.debug.maximum_connection_buffer_size, + unsafe_disable_noise: old_cfg.mixnet.debug.unsafe_disable_noise, + use_legacy_packet_encoding: old_cfg.mixnet.debug.use_legacy_packet_encoding, + }, + }, + storage_paths: NymNodePaths { + keys: KeysPaths { + private_ed25519_identity_key_file: old_cfg + .storage_paths + .keys + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .storage_paths + .keys + .public_ed25519_identity_key_file, + primary_x25519_sphinx_key_file: old_cfg + .storage_paths + .keys + .primary_x25519_sphinx_key_file, + private_x25519_noise_key_file: old_cfg + .storage_paths + .keys + .private_x25519_noise_key_file, + public_x25519_noise_key_file: old_cfg + .storage_paths + .keys + .public_x25519_noise_key_file, + secondary_x25519_sphinx_key_file: old_cfg + .storage_paths + .keys + .secondary_x25519_sphinx_key_file, + }, + description: old_cfg.storage_paths.description, + }, + http: Http { + bind_address: old_cfg.http.bind_address, + landing_page_assets_path: old_cfg.http.landing_page_assets_path, + access_token: old_cfg.http.access_token, + expose_system_info: old_cfg.http.expose_system_info, + expose_system_hardware: old_cfg.http.expose_system_hardware, + expose_crypto_hardware: old_cfg.http.expose_crypto_hardware, + node_load_cache_ttl: old_cfg.http.node_load_cache_ttl, + }, + verloc: Verloc { + bind_address: old_cfg.verloc.bind_address, + announce_port: old_cfg.verloc.announce_port, + debug: VerlocDebug { + packets_per_node: old_cfg.verloc.debug.packets_per_node, + connection_timeout: old_cfg.verloc.debug.connection_timeout, + packet_timeout: old_cfg.verloc.debug.packet_timeout, + delay_between_packets: old_cfg.verloc.debug.delay_between_packets, + tested_nodes_batch_size: old_cfg.verloc.debug.tested_nodes_batch_size, + testing_interval: old_cfg.verloc.debug.testing_interval, + retry_timeout: old_cfg.verloc.debug.retry_timeout, + }, + }, + wireguard: Wireguard { + enabled: old_cfg.wireguard.enabled, + bind_address: old_cfg.wireguard.bind_address, + private_ipv4: old_cfg.wireguard.private_ipv4, + private_ipv6: old_cfg.wireguard.private_ipv6, + announced_tunnel_port: old_cfg.wireguard.announced_tunnel_port, + announced_metadata_port: old_cfg.wireguard.announced_metadata_port, + private_network_prefix_v4: old_cfg.wireguard.private_network_prefix_v4, + private_network_prefix_v6: old_cfg.wireguard.private_network_prefix_v6, + storage_paths: WireguardPaths { + private_diffie_hellman_key_file: old_cfg + .wireguard + .storage_paths + .private_diffie_hellman_key_file, + public_diffie_hellman_key_file: old_cfg + .wireguard + .storage_paths + .public_diffie_hellman_key_file, + }, + }, + gateway_tasks: GatewayTasksConfig { + storage_paths: GatewayTasksPaths { + clients_storage: old_cfg.gateway_tasks.storage_paths.clients_storage, + stats_storage: old_cfg.gateway_tasks.storage_paths.stats_storage, + cosmos_mnemonic: old_cfg.gateway_tasks.storage_paths.cosmos_mnemonic, + bridge_client_params: old_cfg.gateway_tasks.storage_paths.bridge_client_params, + }, + enforce_zk_nyms: old_cfg.gateway_tasks.enforce_zk_nyms, + ws_bind_address: old_cfg.gateway_tasks.ws_bind_address, + announce_ws_port: old_cfg.gateway_tasks.announce_ws_port, + announce_wss_port: old_cfg.gateway_tasks.announce_wss_port, + // \/ ADDED + upgrade_mode: UpgradeModeWatcher::new() + .inspect_err(|_| { + error!( + "failed to set custom upgrade mode configuration - falling back to mainnet" + ) + }) + .unwrap_or(UpgradeModeWatcher::new_mainnet()), + // /\ ADDED + debug: gateway_tasks::Debug { + message_retrieval_limit: old_cfg.gateway_tasks.debug.message_retrieval_limit, + maximum_open_connections: old_cfg.gateway_tasks.debug.maximum_open_connections, + minimum_mix_performance: old_cfg.gateway_tasks.debug.minimum_mix_performance, + max_request_timestamp_skew: old_cfg.gateway_tasks.debug.max_request_timestamp_skew, + stale_messages: StaleMessageDebug { + cleaner_run_interval: old_cfg + .gateway_tasks + .debug + .stale_messages + .cleaner_run_interval, + max_age: old_cfg.gateway_tasks.debug.stale_messages.max_age, + }, + client_bandwidth: ClientBandwidthDebug { + max_flushing_rate: old_cfg + .gateway_tasks + .debug + .client_bandwidth + .max_flushing_rate, + max_delta_flushing_amount: old_cfg + .gateway_tasks + .debug + .client_bandwidth + .max_delta_flushing_amount, + }, + zk_nym_tickets: ZkNymTicketHandlerDebug { + revocation_bandwidth_penalty: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .revocation_bandwidth_penalty, + pending_poller: old_cfg.gateway_tasks.debug.zk_nym_tickets.pending_poller, + minimum_api_quorum: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .minimum_api_quorum, + minimum_redemption_tickets: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .minimum_redemption_tickets, + maximum_time_between_redemption: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .maximum_time_between_redemption, + }, + // \/ ADDED (be explicit about the value rather than using ..Default::default() + upgrade_mode_min_staleness_recheck: gateway_tasks::Debug::default() + .upgrade_mode_min_staleness_recheck, + // /\ ADDED + }, + }, + service_providers: ServiceProvidersConfig { + storage_paths: ServiceProvidersPaths { + clients_storage: old_cfg.service_providers.storage_paths.clients_storage, + stats_storage: old_cfg.service_providers.storage_paths.stats_storage, + network_requester: NetworkRequesterPaths { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .network_requester + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .network_requester + .gateway_registrations, + }, + ip_packet_router: IpPacketRouterPaths { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .gateway_registrations, + }, + authenticator: AuthenticatorPaths { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .authenticator + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .authenticator + .gateway_registrations, + }, + }, + open_proxy: old_cfg.service_providers.open_proxy, + upstream_exit_policy_url: old_cfg.service_providers.upstream_exit_policy_url, + network_requester: NetworkRequester { + debug: NetworkRequesterDebug { + enabled: old_cfg.service_providers.network_requester.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .network_requester + .debug + .disable_poisson_rate, + client_debug: old_cfg + .service_providers + .network_requester + .debug + .client_debug, + }, + }, + ip_packet_router: IpPacketRouter { + debug: IpPacketRouterDebug { + enabled: old_cfg.service_providers.ip_packet_router.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .ip_packet_router + .debug + .disable_poisson_rate, + client_debug: old_cfg + .service_providers + .ip_packet_router + .debug + .client_debug, + }, + }, + authenticator: Authenticator { + debug: AuthenticatorDebug { + enabled: old_cfg.service_providers.authenticator.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .authenticator + .debug + .disable_poisson_rate, + client_debug: old_cfg.service_providers.authenticator.debug.client_debug, + }, + }, + debug: service_providers::Debug { + message_retrieval_limit: old_cfg.service_providers.debug.message_retrieval_limit, + }, + }, + metrics: Default::default(), + logging: LoggingSettings {}, + debug: Default::default(), + }; + Ok(cfg) +} diff --git a/nym-node/src/config/upgrade_helpers.rs b/nym-node/src/config/upgrade_helpers.rs index 63dda0d6e8..b2b8018231 100644 --- a/nym-node/src/config/upgrade_helpers.rs +++ b/nym-node/src/config/upgrade_helpers.rs @@ -17,7 +17,8 @@ async fn try_upgrade_config(path: &Path) -> Result<(), NymNodeError> { let cfg = try_upgrade_config_v7(path, cfg).await.ok(); let cfg = try_upgrade_config_v8(path, cfg).await.ok(); let cfg = try_upgrade_config_v9(path, cfg).await.ok(); - match try_upgrade_config_v10(path, cfg).await { + let cfg = try_upgrade_config_v10(path, cfg).await.ok(); + match try_upgrade_config_v11(path, cfg).await { Ok(cfg) => cfg.save(), Err(e) => { tracing::error!("Failed to finish upgrade: {e}"); From b68e13f0f26e098d8578ca77e013abf2a121e277 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Tue, 13 Jan 2026 16:47:13 +0100 Subject: [PATCH 180/180] update changelog --- CHANGELOG.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2033d13fe5..388dc6d5d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,62 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [Unreleased] +## [2026.1-niolo] (2026-01-13) + +- bugfix: mozzarella -> niolo config migration ([#6259]) +- chore: remove run DKG migration ([#6253]) +- bugfix: reexposed 'derive_extended_private_key' ([#6247]) +- Bump js-yaml from 3.14.1 to 3.14.2 in /sdk/typescript/codegen/contract-clients ([#6231]) +- Statistics API v2 ([#6227]) +- Bump golang.org/x/crypto from 0.39.0 to 0.45.0 in /nym-gateway-probe/netstack_ping ([#6220]) +- Update chain registry link ([#6219]) +- Bump glob from 10.3.4 to 10.5.0 in /documentation/scripts/post-process ([#6216]) +- Bump js-yaml from 4.1.0 to 4.1.1 in /sdk/typescript/tests/integration-tests/mix-fetch ([#6215]) +- gateway-probe fixes for run-local ([#6212]) +- chore: updated default endpoint for retrieving attestation.json ([#6207]) +- chore: remove support for legacy mixnode within the performance contract ([#6205]) +- feat: upgrade mode: VPN adjustments ([#6189]) +- Bump min-document from 2.19.0 to 2.19.1 ([#6181]) +- Bump next from 15.4.1 to 15.4.7 in /nym-node-status-api/nym-node-status-ui ([#6180]) +- feat: merge intermediate upgrade mode changes ([#6174]) +- Add weighted scoring to NS API ([#6144]) +- build(deps): bump mikefarah/yq from 4.47.1 to 4.48.1 ([#6107]) +- build(deps): bump SonarSource/sonarqube-scan-action from 5 to 6 in /.github/workflows ([#6068]) +- build(deps): bump tar-fs from 3.0.9 to 3.1.1 in /sdk/typescript/tests/integration-tests/mix-fetch ([#6063]) +- build(deps): bump ammonia from 4.1.1 to 4.1.2 ([#6057]) +- build(deps): bump tower-http from 0.5.2 to 0.6.6 ([#6030]) +- build(deps): bump actions/setup-go from 5 to 6 ([#6013]) +- build(deps): bump next from 14.2.28 to 14.2.32 ([#5996]) +- build(deps): bump tracing-subscriber from 0.3.19 to 0.3.20 ([#5993]) +- build(deps): bump actions/upload-pages-artifact from 3 to 4 ([#5992]) + +[#6259]: https://github.com/nymtech/nym/pull/6259 +[#6253]: https://github.com/nymtech/nym/pull/6253 +[#6247]: https://github.com/nymtech/nym/pull/6247 +[#6231]: https://github.com/nymtech/nym/pull/6231 +[#6227]: https://github.com/nymtech/nym/pull/6227 +[#6220]: https://github.com/nymtech/nym/pull/6220 +[#6219]: https://github.com/nymtech/nym/pull/6219 +[#6216]: https://github.com/nymtech/nym/pull/6216 +[#6215]: https://github.com/nymtech/nym/pull/6215 +[#6212]: https://github.com/nymtech/nym/pull/6212 +[#6207]: https://github.com/nymtech/nym/pull/6207 +[#6205]: https://github.com/nymtech/nym/pull/6205 +[#6189]: https://github.com/nymtech/nym/pull/6189 +[#6181]: https://github.com/nymtech/nym/pull/6181 +[#6180]: https://github.com/nymtech/nym/pull/6180 +[#6174]: https://github.com/nymtech/nym/pull/6174 +[#6144]: https://github.com/nymtech/nym/pull/6144 +[#6107]: https://github.com/nymtech/nym/pull/6107 +[#6068]: https://github.com/nymtech/nym/pull/6068 +[#6063]: https://github.com/nymtech/nym/pull/6063 +[#6057]: https://github.com/nymtech/nym/pull/6057 +[#6030]: https://github.com/nymtech/nym/pull/6030 +[#6013]: https://github.com/nymtech/nym/pull/6013 +[#5996]: https://github.com/nymtech/nym/pull/5996 +[#5993]: https://github.com/nymtech/nym/pull/5993 +[#5992]: https://github.com/nymtech/nym/pull/5992 + ## [2025.21-mozzarella] (2025-11-25) - [bugfix] Tunnel not waiting on MixnetClient to shut down cleanly ([#6225])