diff --git a/.github/workflows/build-nym-vpn-android.yml b/.github/workflows/build-nym-vpn-android.yml index 7eeabd9779..12daad5b18 100644 --- a/.github/workflows/build-nym-vpn-android.yml +++ b/.github/workflows/build-nym-vpn-android.yml @@ -63,7 +63,8 @@ env: jobs: build: - runs-on: arc-linux-latest + runs-on: AppleSilicon + env: SIGNING_KEY_ALIAS: ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }} SIGNING_KEY_PASSWORD: ${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }} @@ -101,7 +102,12 @@ jobs: shell: bash run: | echo "ANDROID_NDK_HOME=${{ steps.setup-ndk.outputs.ndk-path }}" >> $GITHUB_ENV - echo "NDK_TOOLCHAIN_DIR=${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $GITHUB_ENV + case "$(uname -s)" in + Linux*) host_tag=linux-x86_64 ;; + Darwin*) host_tag=darwin-x86_64 ;; + *) host_tag=linux-x86_64 ;; + esac + echo "NDK_TOOLCHAIN_DIR=${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/${host_tag}/bin" >> $GITHUB_ENV - name: Grant execute permission for gradlew run: chmod +x gradlew @@ -129,10 +135,26 @@ jobs: with: crate: cargo-license - - name: Install deps + - name: Install deps (Linux) + if: runner.os == 'Linux' run: | - sudo apt-get update && sudo apt-get install -y libdbus-1-dev libmnl-dev libnftnl-dev protobuf-compiler git curl gcc g++ make unzip rsync + sudo apt-get update && sudo apt-get install -y \ + libdbus-1-dev libmnl-dev libnftnl-dev protobuf-compiler \ + git curl gcc g++ make unzip rsync + - name: Install deps (macOS) + if: runner.os == 'macOS' + run: | + brew update + brew install \ + protobuf \ + git \ + curl \ + gcc \ + make \ + unzip \ + rsync || true + # Here we need to decode keystore.jks from base64 string and place it # in the folder specified in the release signing configuration - name: Decode Keystore diff --git a/.pkg/linux/install b/.pkg/linux/install index 1c281c76f6..18f5cbfaec 100755 --- a/.pkg/linux/install +++ b/.pkg/linux/install @@ -53,17 +53,17 @@ CORE_BUILD=${CORE_BUILD:-0} ## STABLE BUILDS ################################################# # app artifacts -app_tag=nym-vpn-app-v1.19.0 -app_version=1.19.0 +app_tag=nym-vpn-app-v1.20.0 +app_version=1.20.0 appimage_url="https://github.com/nymtech/nym-vpn-client/releases/download/$app_tag/NymVPN_${app_version}_x64.AppImage" app_deb_url="https://github.com/nymtech/nym-vpn-client/releases/download/$app_tag/nym-vpn-app_${app_version}_amd64.deb" desktop_url="https://raw.githubusercontent.com/nymtech/nym-vpn-client/ac4153bb1/nym-vpn-app/.pkg/app.desktop" icon_url="https://raw.githubusercontent.com/nymtech/nym-vpn-client/fb526935/nym-vpn-app/.pkg/icon.svg" # nym-vpnd artifacts -vpnd_tag=nym-vpn-core-v1.19.0 +vpnd_tag=nym-vpn-core-v1.20.0 # ⚠ there are inconsistencies in the package version naming… -vpnd_version=1.19.0 +vpnd_version=1.20.0 vpnd_deb_ver=$vpnd_version vpnd_deb_ver_url=$vpnd_version vpnd_raw_url="https://github.com/nymtech/nym-vpn-client/releases/download/$vpnd_tag/nym-vpn-core-v${vpnd_version}_linux_x86_64.tar.gz" diff --git a/.pkg/tauri-updater/stable.json b/.pkg/tauri-updater/stable.json index bbf51727db..998f281462 100644 --- a/.pkg/tauri-updater/stable.json +++ b/.pkg/tauri-updater/stable.json @@ -1,9 +1,9 @@ { - "version": "1.19.0", + "version": "1.20.0", "platforms": { "windows-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTTjN5M3FybWZ5VjJvNFU3cjhES2V2MWJXUEpBaGw2RGZVMUNleHpyc1ZVY3dhazVZYlZEMkgzMk1MNytBTTUweGFlOWl0UzVUdS85VE8yd2FyNG5IMmxaTkF5WnA5UFFnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzYzNTQ0MzgwCWZpbGU6TnltVlBOXzEuMTkuMF94NjQtc2V0dXAuZXhlClFZV1N1WmYvbkJsUDduYkw2NXJwT1ZIYXNmbTVDZnVxMER4WWUrdWlMWGVDNzlhdk1uakRHdWdadURuRCtWVlBvdzdQQmxwejY4UFEydHdkZWNpWkFnPT0K", - "url": "https://github.com/nymtech/nym-vpn-client/releases/download/nym-vpn-app-v1.19.0/NymVPN_1.19.0_x64-setup.exe" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTTjN5M3FybWZ5VjdtYzlYSDdYNGlIQnRQaDJQQTRHaGh1c2o5cGszM1dXVUZwODUxZEJPdS9OWi9rVytsZDluaTUweGorQ0h6RUlBUk5RQjY4TS92QlVENHpVK2U5NXdBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzY0Njk3MDAwCWZpbGU6TnltVlBOXzEuMjAuMF94NjQtc2V0dXAuZXhlCjBFWURWWVd6NndmOC9adlBOV1ByN2JIVnpjZWxhTDB5V1haM3ZLdUwyNWJxQ3J4Qm9PUFYrYmdHVEw0dEVOdjFZL1hFV1dZNk90NXEzMk9NQUpaOENRPT0K", + "url": "https://github.com/nymtech/nym-vpn-client/releases/download/nym-vpn-app-v1.20.0/NymVPN_1.20.0_x64-setup.exe" } } } diff --git a/fastlane/metadata/android/de-DE/short_description.txt b/fastlane/metadata/android/de-DE/short_description.txt index 8d01f43568..822da3cd06 100644 --- a/fastlane/metadata/android/de-DE/short_description.txt +++ b/fastlane/metadata/android/de-DE/short_description.txt @@ -1 +1 @@ -Die weltweit einzige VPN-App, die dich nicht ausspionieren kann. Akademisch gestaltet, Schweizer gemacht. \ No newline at end of file +Die einzige VPN-App, die dich nicht ausspioniert – akademisch & schweizerisch. \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/25000.txt b/fastlane/metadata/android/en-US/changelogs/25000.txt new file mode 100644 index 0000000000..cc2db04dad --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/25000.txt @@ -0,0 +1,5 @@ +What's new: +- Added node details to quick settings and a new Account screen +- Introduced bi-mode split tunneling on Android +- Updated Settings, Privacy, Censorship, and navigation UI +- Improved connection stability and error handling \ No newline at end of file diff --git a/fastlane/metadata/android/es-ES/short_description.txt b/fastlane/metadata/android/es-ES/short_description.txt index 0f6e666293..8f718ae81b 100644 --- a/fastlane/metadata/android/es-ES/short_description.txt +++ b/fastlane/metadata/android/es-ES/short_description.txt @@ -1 +1 @@ -La única aplicación VPN del mundo que no puede espiarte. Académicamente diseñado, fabricado en Suiza. \ No newline at end of file +La VPN única que no puede espiarte, diseñada académicamente y hecha en Suiza. \ No newline at end of file diff --git a/fastlane/metadata/android/fa/short_description.txt b/fastlane/metadata/android/fa/short_description.txt index d85083401b..0b29f2b9e6 100644 --- a/fastlane/metadata/android/fa/short_description.txt +++ b/fastlane/metadata/android/fa/short_description.txt @@ -1 +1 @@ -تنها برنامهٔ VPN در جهان که توانایی جاسوسی از شما را ندارد. طراحی علمی با ساخت کشور سوئیس. \ No newline at end of file +تنها VPN‌ای که نمی‌تواند از شما جاسوسی کند؛ طراحی علمی و ساختِ سوئیس. \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/short_description.txt b/fastlane/metadata/android/fr-FR/short_description.txt index 1da1c6ecd3..6b8ee6f357 100644 --- a/fastlane/metadata/android/fr-FR/short_description.txt +++ b/fastlane/metadata/android/fr-FR/short_description.txt @@ -1 +1 @@ -La seule application VPN au monde qui ne peut pas vous espionner. Elaboré par des universitaires, Swiss made . \ No newline at end of file +La seule VPN qui ne peut pas vous espionner, conçue par des experts suisses. \ No newline at end of file diff --git a/fastlane/metadata/android/hi-IN/short_description.txt b/fastlane/metadata/android/hi-IN/short_description.txt index 8936f0bebb..8d7faf40d3 100644 --- a/fastlane/metadata/android/hi-IN/short_description.txt +++ b/fastlane/metadata/android/hi-IN/short_description.txt @@ -1 +1 @@ -दुनिया का एकमात्र VPN ऐप जो आप पर जासूसी नहीं कर सकता। अकादमिक रूप से तैयार किया गया, स्विस कारीगरी से बना। \ No newline at end of file +एकमात्र VPN जो आप पर जासूसी नहीं कर सकता — अकादमिक डिज़ाइन, स्विस बना। \ No newline at end of file diff --git a/fastlane/metadata/android/ru-RU/short_description.txt b/fastlane/metadata/android/ru-RU/short_description.txt index 0c2ac425e0..fa188ed81e 100644 --- a/fastlane/metadata/android/ru-RU/short_description.txt +++ b/fastlane/metadata/android/ru-RU/short_description.txt @@ -1 +1 @@ -Единственное в мире приложение VPN, которое не может шпионить за вами. Разработано учёными, создано в Швейцарии. \ No newline at end of file +Единственный VPN, который не может шпионить за вами — создан учёными Швейцарии. \ No newline at end of file diff --git a/fastlane/metadata/android/tr-TR/short_description.txt b/fastlane/metadata/android/tr-TR/short_description.txt index 070f9a27ec..269bbf7802 100644 --- a/fastlane/metadata/android/tr-TR/short_description.txt +++ b/fastlane/metadata/android/tr-TR/short_description.txt @@ -1 +1 @@ -Dünyadaki tek VPN uygulaması—sizi gözetlemesi imkânsız. Akademik tasarım, İsviçre menşeeli. \ No newline at end of file +Gözetleyemeyen tek VPN: akademik tasarım, İsviçre üretimi. \ No newline at end of file diff --git a/fastlane/metadata/android/uk/short_description.txt b/fastlane/metadata/android/uk/short_description.txt index 419c61dc9f..68fd420c81 100644 --- a/fastlane/metadata/android/uk/short_description.txt +++ b/fastlane/metadata/android/uk/short_description.txt @@ -1 +1 @@ -Єдиний у світі VPN, який не може шпигувати за вами. Розроблено в академічних колах, виготовлено в Швейцарії. \ No newline at end of file +Єдиний VPN, що не може шпигувати за вами — академічна розробка зі Швейцарії. \ No newline at end of file diff --git a/fastlane/metadata/android/vi/short_description.txt b/fastlane/metadata/android/vi/short_description.txt index ebb14bab0f..964772d59c 100644 --- a/fastlane/metadata/android/vi/short_description.txt +++ b/fastlane/metadata/android/vi/short_description.txt @@ -1 +1 @@ -Ứng dụng VPN duy nhất trên thế giới không thể theo dõi bạn. Thiết kế học thuật, sản xuất tại Thụy Sĩ. \ No newline at end of file +VPN duy nhất không thể theo dõi bạn — thiết kế học thuật, sản xuất Thụy Sĩ. \ No newline at end of file diff --git a/nym-vpn-android/CHANGELOG.md b/nym-vpn-android/CHANGELOG.md index 27380f2f23..5eca2ee2a0 100644 --- a/nym-vpn-android/CHANGELOG.md +++ b/nym-vpn-android/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.5.0] - 2025-12-03 + ### Added - Node details added in quick settings notification (https://github.com/nymtech/nym-vpn-client/pull/4029) - AmneziaWG section added to censorship screen (https://github.com/nymtech/nym-vpn-client/pull/4033) @@ -14,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Account screen added (https://github.com/nymtech/nym-vpn-client/pull/4060) - Android: Bi-mode Split Tunneling (https://github.com/nymtech/nym-vpn-client/pull/4049 - "Help with translation" link added to Languages screen (https://github.com/nymtech/nym-vpn-client/pull/4086) +- Allow user to logout when tunnel is Up (https://github.com/nymtech/nym-vpn-client/pull/4103) ### Changed - UI changes and updates for Settings screen (https://github.com/nymtech/nym-vpn-client/pull/4060) @@ -27,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix QUIC autostart issue (https://github.com/nymtech/nym-vpn-client/pull/4040) - Fix for Sentry toggle (https://github.com/nymtech/nym-vpn-client/pull/4060) -## [2.4.0] +## [2.4.0] - 2025-11-19 ### Added - Crowdin Translations (https://github.com/nymtech/nym-vpn-client/pull/2777) diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/AppViewModel.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/AppViewModel.kt index be48d07810..f4dbfa84cb 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/AppViewModel.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/AppViewModel.kt @@ -73,17 +73,20 @@ constructor( fun logout(onComplete: (() -> Unit)? = null) = viewModelScope.launch { runCatching { if (backendManager.getState() == Tunnel.State.Down) { - backendManager.removeMnemonic() - backendManager.refresh() - onComplete?.invoke() + performLogout(onComplete) } else { - SnackbarController.showMessage( - StringValue.StringResource(R.string.action_requires_tunnel_down), - ) + backendManager.stopTunnel() + performLogout(onComplete) } }.onFailure { Timber.e(it) } } + private suspend fun performLogout(onComplete: (() -> Unit)? = null) { + backendManager.removeMnemonic() + backendManager.refresh() + onComplete?.invoke() + } + fun onLocaleChange(localeTag: String) = viewModelScope.launch { settingsRepository.setLocale(localeTag) LocaleUtil.changeLocale(localeTag) diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/SettingsScreen.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/SettingsScreen.kt index 6b1d156c77..a80ee1507d 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/SettingsScreen.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/SettingsScreen.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import net.nymtech.nymvpn.BuildConfig -import net.nymtech.nymvpn.R import net.nymtech.nymvpn.ui.AppUiState import net.nymtech.nymvpn.ui.AppViewModel import net.nymtech.nymvpn.ui.Route @@ -46,7 +45,6 @@ import net.nymtech.nymvpn.util.extensions.launchBatteryOptSettingsScreen import net.nymtech.nymvpn.util.extensions.launchNotificationSettings import net.nymtech.nymvpn.util.extensions.launchVpnSettings import net.nymtech.nymvpn.util.extensions.scaledWidth -import net.nymtech.vpn.backend.Tunnel import kotlin.Boolean @Composable @@ -120,11 +118,7 @@ fun SettingsScreen(appViewModel: AppViewModel, appUiState: AppUiState, showVpnSe onSystemStatusClick = { }, onLogoutClick = { - if (appUiState.managerState.tunnelState != Tunnel.State.Down) { - snackbar.showMessage(context.getString(R.string.action_requires_tunnel_down)) - } else { - showLogoutDialog = true - } + showLogoutDialog = true }, onQuitClick = { (context as Activity).finishAffinity() diff --git a/nym-vpn-android/buildSrc/src/main/kotlin/Constants.kt b/nym-vpn-android/buildSrc/src/main/kotlin/Constants.kt index 34bf049b6a..f3885ecc32 100644 --- a/nym-vpn-android/buildSrc/src/main/kotlin/Constants.kt +++ b/nym-vpn-android/buildSrc/src/main/kotlin/Constants.kt @@ -1,8 +1,8 @@ import org.gradle.api.JavaVersion object Constants { - const val VERSION_NAME = "v2.4.0" - const val VERSION_CODE = 24000 + const val VERSION_NAME = "v2.5.0" + const val VERSION_CODE = 25000 const val TARGET_SDK = 36 const val COMPILE_SDK = 36 const val MIN_SDK = 24 diff --git a/nym-vpn-app/.pkg/flatpak/net.nymtech.NymVPN.metainfo.xml b/nym-vpn-app/.pkg/flatpak/net.nymtech.NymVPN.metainfo.xml index 93471b0a40..4a4bd72c6b 100644 --- a/nym-vpn-app/.pkg/flatpak/net.nymtech.NymVPN.metainfo.xml +++ b/nym-vpn-app/.pkg/flatpak/net.nymtech.NymVPN.metainfo.xml @@ -49,6 +49,9 @@

+ + https://github.com/nymtech/nym-vpn-client/releases/tag/nym-vpn-app-v1.20.0 + https://github.com/nymtech/nym-vpn-client/releases/tag/nym-vpn-app-v1.19.0 diff --git a/nym-vpn-app/CHANGELOG.md b/nym-vpn-app/CHANGELOG.md index 709074bab9..53fc8f53ef 100644 --- a/nym-vpn-app/CHANGELOG.md +++ b/nym-vpn-app/CHANGELOG.md @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.20.0] - 2025-12-02 + +### Added + +- Allow seeing server details from home screen +- Auto focus node list search input +- Minimal obfuscation (AmneziaWG) +- Allow to connect dApps / wallets to the mixnet via SOCKS5 url or HTTP RPC url + +### Changed + +- Always show Anti-Censorship setting option +- Visual improvements + +### Fixed + +- Styling fixes and visual improvements + ## [1.19.0] - 2025-11-19 ### Added diff --git a/nym-vpn-app/package-lock.json b/nym-vpn-app/package-lock.json index a683494720..fc9edcae74 100644 --- a/nym-vpn-app/package-lock.json +++ b/nym-vpn-app/package-lock.json @@ -10,6 +10,9 @@ "license": "GPL-3.0-only", "dependencies": { "@base-ui-components/react": "^1.0.0-beta.3", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@headlessui/react": "^2.1.2", "@lottiefiles/dotlottie-react": "^0.17.0", "@radix-ui/react-accordion": "^1.2.3", @@ -383,6 +386,60 @@ } } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", diff --git a/nym-vpn-app/package.json b/nym-vpn-app/package.json index 9d49b565bb..e00312db8d 100644 --- a/nym-vpn-app/package.json +++ b/nym-vpn-app/package.json @@ -31,6 +31,9 @@ }, "dependencies": { "@base-ui-components/react": "^1.0.0-beta.3", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@headlessui/react": "^2.1.2", "@lottiefiles/dotlottie-react": "^0.17.0", "@radix-ui/react-accordion": "^1.2.3", diff --git a/nym-vpn-app/src-tauri/src/commands/tunnel.rs b/nym-vpn-app/src-tauri/src/commands/tunnel.rs index 7046fbfc3c..3183237e4d 100644 --- a/nym-vpn-app/src-tauri/src/commands/tunnel.rs +++ b/nym-vpn-app/src-tauri/src/commands/tunnel.rs @@ -11,6 +11,7 @@ use crate::{ }; use tauri::{Manager, State}; use tracing::{debug, info, instrument, warn}; +use std::net::IpAddr; #[instrument(skip_all)] #[tauri::command] @@ -153,3 +154,24 @@ pub async fn set_allow_lan(vpnd: State<'_, VpndClient>, enabled: bool) -> Result vpnd.set_allow_lan(enabled).await?; Ok(()) } + +#[instrument(skip(vpnd))] +#[tauri::command] +pub async fn get_default_dns(vpnd: State<'_, VpndClient>) -> Result, BackendError> { + let dns = vpnd.get_default_dns().await?; + Ok(dns) +} + +#[instrument(skip(vpnd))] +#[tauri::command] +pub async fn set_custom_dns_enabled(vpnd: State<'_, VpndClient>, enabled: bool) -> Result<(), BackendError> { + vpnd.set_custom_dns_enabled(enabled).await?; + Ok(()) +} + +#[instrument(skip(vpnd))] +#[tauri::command] +pub async fn set_custom_dns(vpnd: State<'_, VpndClient>, dns: Vec) -> Result<(), BackendError> { + vpnd.set_custom_dns(dns).await?; + Ok(()) +} diff --git a/nym-vpn-app/src-tauri/src/main.rs b/nym-vpn-app/src-tauri/src/main.rs index 8022f4437f..31a240d520 100644 --- a/nym-vpn-app/src-tauri/src/main.rs +++ b/nym-vpn-app/src-tauri/src/main.rs @@ -263,6 +263,9 @@ async fn main() -> Result<()> { tunnel::disconnect, tunnel::set_node, tunnel::set_quic, + tunnel::get_default_dns, + tunnel::set_custom_dns, + tunnel::set_custom_dns_enabled, tunnel::set_no_ipv6, tunnel::set_allow_lan, cmd_db::db_set, diff --git a/nym-vpn-app/src-tauri/src/vpnd/client.rs b/nym-vpn-app/src-tauri/src/vpnd/client.rs index 125897ecec..81324be92b 100644 --- a/nym-vpn-app/src-tauri/src/vpnd/client.rs +++ b/nym-vpn-app/src-tauri/src/vpnd/client.rs @@ -23,6 +23,7 @@ use std::{ env::consts::{ARCH, OS}, path::PathBuf, sync::Mutex, + net::IpAddr, }; use tauri::{AppHandle, Manager, PackageInfo}; use tokio_stream::StreamExt; @@ -360,7 +361,7 @@ impl VpndClient { pub async fn vpn_connect(&self) -> Result<(), VpndError> { let mut vpnd = self.vpnd().await?; - vpnd.connect_tunnel_v2() + vpnd.connect_tunnel() .await .map_err(VpndError::RpcClient) .inspect_err(|e| { @@ -773,6 +774,49 @@ impl VpndClient { Ok(()) } + #[instrument(skip_all)] + pub async fn get_default_dns(&self) -> Result, VpndError> { + let mut vpnd = self.vpnd().await?; + + let dns = vpnd.get_default_dns().await.map_err(|e| { + error!("failed to get default DNS: {}", e); + VpndError::RpcClient(e) + })?; + Ok(dns) + } + + #[instrument(skip_all)] + pub async fn set_custom_dns_enabled(&self, enabled: bool) -> Result<(), VpndError> { + let mut vpnd = self.vpnd().await?; + + vpnd.set_enable_custom_dns(enabled).await.map_err(VpndError::RpcClient).inspect_err(|e| { + error!("failed to set custom DNS enabled: {}", e); + })?; + + + debug!("custom DNS enabled: {}", enabled); + if enabled { + info!("⚠ vpnd custom DNS enabled ⚠"); + } else { + info!("custom DNS disabled"); + } + Ok(()) + } + + #[instrument(skip_all)] + pub async fn set_custom_dns(&self, dns: Vec) -> Result<(), VpndError> { + let mut vpnd = self.vpnd().await?; + + let borrowed_dns = dns.clone(); + + vpnd.set_custom_dns(dns).await.map_err(VpndError::RpcClient).inspect_err(|e| { + error!("failed to set custom DNS: {}", e); + })?; + + debug!("custom DNS set: {:?}", borrowed_dns); + Ok(()) + } + pub fn reset_log_flag() { let mut logged = VPND_DOWN_LOGGED.lock().unwrap(); *logged = false; diff --git a/nym-vpn-app/src-tauri/src/vpnd/config.rs b/nym-vpn-app/src-tauri/src/vpnd/config.rs index 06b8a0e098..8d02212f2b 100644 --- a/nym-vpn-app/src-tauri/src/vpnd/config.rs +++ b/nym-vpn-app/src-tauri/src/vpnd/config.rs @@ -13,6 +13,7 @@ pub struct VpndConfig { pub entry_node: Node, pub exit_node: Node, pub custom_dns: Option>, + pub enable_custom_dns: bool, pub allow_lan: bool, pub disable_ipv6: bool, pub vpn_mode: VpnMode, @@ -37,7 +38,8 @@ impl VpndConfig { Ok(VpndConfig { entry_node: config.entry_point.into(), exit_node: config.exit_point.try_into()?, - custom_dns: config.custom_dns, + custom_dns: Some(config.custom_dns), + enable_custom_dns: config.enable_custom_dns, allow_lan: config.allow_lan, disable_ipv6: config.disable_ipv6, vpn_mode, diff --git a/nym-vpn-app/src/constants.ts b/nym-vpn-app/src/constants.ts index 0c0ca8d94a..3d21eb5e83 100644 --- a/nym-vpn-app/src/constants.ts +++ b/nym-vpn-app/src/constants.ts @@ -56,3 +56,4 @@ export const ResidentialIpServersUrl = 'https://support.nym.com/hc/en-us/articles/35279486714641-Why-can-t-I-access-streaming-services-while-using-NymVPN'; export const QuicSupportArticleUrl = 'https://support.nym.com/hc/en-us/articles/39648047741457-QUIC-transport-mode'; +export const CustomDnsHelpUrl = 'https://nym.com/features/custom-dns'; diff --git a/nym-vpn-app/src/contexts/main/provider.tsx b/nym-vpn-app/src/contexts/main/provider.tsx index fdb285c1ab..6c551bf737 100644 --- a/nym-vpn-app/src/contexts/main/provider.tsx +++ b/nym-vpn-app/src/contexts/main/provider.tsx @@ -28,6 +28,8 @@ function MainStateProvider({ children, init }: Props) { quic: init.quic, ipv6Support: !init.noIpv6, allowLan: init.allowLan, + customDnsEnabled: init.customDnsEnabled, + customDns: init.customDns, }); const { push } = useInAppNotify(); diff --git a/nym-vpn-app/src/contexts/main/reducer.ts b/nym-vpn-app/src/contexts/main/reducer.ts index 6b435e86fe..2401b5da7a 100644 --- a/nym-vpn-app/src/contexts/main/reducer.ts +++ b/nym-vpn-app/src/contexts/main/reducer.ts @@ -76,7 +76,10 @@ export type StateAction = | { type: 'set-backend-flags'; flags: FeatureFlags } | { type: 'set-quic'; enabled: boolean } | { type: 'set-domain-fronting'; enabled: boolean } - | { type: 'set-streaming-optimized-label-seen'; seen: boolean }; + | { type: 'set-streaming-optimized-label-seen'; seen: boolean } + | { type: 'set-custom-dns-enabled'; enabled: boolean } + | { type: 'set-custom-dns'; dns: string[] } + | { type: 'set-default-dns'; dns: string[] }; export const initialState: AppState = { initialized: false, @@ -115,6 +118,9 @@ export const initialState: AppState = { zknymCredential: false, }, streamingOptimizedLabelSeen: false, + customDnsEnabled: false, + customDns: [], + defaultDns: [], }; export function reducer(state: AppState, action: StateAction): AppState { @@ -380,7 +386,21 @@ export function reducer(state: AppState, action: StateAction): AppState { ...state, domainFronting: action.enabled, }; - + case 'set-custom-dns-enabled': + return { + ...state, + customDnsEnabled: action.enabled, + }; + case 'set-custom-dns': + return { + ...state, + customDns: action.dns, + }; + case 'set-default-dns': + return { + ...state, + defaultDns: action.dns, + }; case 'reset': return initialState; } diff --git a/nym-vpn-app/src/hooks/useCustomDns.ts b/nym-vpn-app/src/hooks/useCustomDns.ts new file mode 100644 index 0000000000..1458822d9f --- /dev/null +++ b/nym-vpn-app/src/hooks/useCustomDns.ts @@ -0,0 +1,48 @@ +import { invoke } from '@tauri-apps/api/core'; +import { useMainDispatch, useMainState } from '../contexts/main/context'; +import { StateDispatch } from '../types'; +import { useInAppNotify } from '../contexts/index'; + +function useCustomDns() { + const { customDnsEnabled, customDns, defaultDns } = useMainState(); + const dispatch = useMainDispatch() as StateDispatch; + const { push } = useInAppNotify(); + + const toggle = async (state: boolean) => { + try { + await invoke('set_custom_dns_enabled', { enabled: state }); + dispatch({ type: 'set-custom-dns-enabled', enabled: state }); + } catch (e) { + console.error(e); + push({ + message: 'Failed to apply DNS changes', + close: true, + type: 'error', + }); + } + }; + + const setCustomDns = async (dns: string[]) => { + try { + await invoke('set_custom_dns', { dns: dns }); + dispatch({ type: 'set-custom-dns', dns: dns }); + } catch (e) { + console.error(e); + push({ + message: 'Failed to apply DNS changes', + close: true, + type: 'error', + }); + } + }; + + return { + enabled: customDnsEnabled, + toggle, + customDns, + defaultDns, + setCustomDns, + }; +} + +export default useCustomDns; diff --git a/nym-vpn-app/src/i18n/en/settings.json b/nym-vpn-app/src/i18n/en/settings.json index 9df6eaab91..a1d920c899 100644 --- a/nym-vpn-app/src/i18n/en/settings.json +++ b/nym-vpn-app/src/i18n/en/settings.json @@ -8,8 +8,8 @@ "desc": "Allow IPv6 connections" }, "allow-lan": { - "title": "Allow LAN traffic", - "desc": "Allow local network access" + "title": "Bypass LAN", + "desc": "Allow direct local network access" }, "notifications": { "title": "Desktop notifications" @@ -32,6 +32,26 @@ "description": "Make the app better in your language." } }, + "dns": { + "title": "Customize DNS", + "topbar-title": "DNS customization", + "details": { + "title": "Customize DNS", + "list-title": "List of DNS servers", + "add": "Add", + "input-label": "DNS address", + "input-placeholder": "IPv4 or IPv6 address", + "apply": "Apply changes", + "warning": "⚠️ Third-party DNS may limit features.", + "link": "Learn more about DNS", + "on": { + "description": "Configure your DNS servers below. Servers will be tried in order as fallbacks. Drag to reorder priority." + }, + "off": { + "description": "Using NymVPN's default DNS optimized for the Nym network." + } + } + }, "legal": { "title": "Legal", "policy": "Privacy statement", diff --git a/nym-vpn-app/src/main.tsx b/nym-vpn-app/src/main.tsx index f962684344..c7a6e98dd7 100644 --- a/nym-vpn-app/src/main.tsx +++ b/nym-vpn-app/src/main.tsx @@ -81,6 +81,7 @@ dayjs.extend(duration); } const config = await invoke('get_vpn_config'); + console.log('config', config); // pre-get and prepare some early stage state const initState: InitState = { @@ -95,6 +96,9 @@ dayjs.extend(duration); config?.disableIpv6 !== undefined ? config.disableIpv6 : defaultNoIpv6, allowLan: config?.allowLan !== undefined ? config.allowLan : defaultAllowLan, + customDnsEnabled: + config?.enableCustomDns !== undefined ? config.enableCustomDns : false, + customDns: !config?.customDns ? [] : config.customDns, }; console.log('initial state:', initState); diff --git a/nym-vpn-app/src/router.tsx b/nym-vpn-app/src/router.tsx index 3b78d80c17..4129fd9933 100644 --- a/nym-vpn-app/src/router.tsx +++ b/nym-vpn-app/src/router.tsx @@ -5,6 +5,7 @@ import { AntiCensorship, Appearance, AppearanceRouteIndex, + CustomDNS, DataAndPrivacy, Dev, Display, @@ -40,6 +41,7 @@ export const routes = { display: '/settings/appearance/display', lang: '/settings/appearance/lang', logs: '/settings/logs', + dns: '/settings/dns', antiCensorship: '/settings/anti-censorship', socks5: '/settings/socks5', dataPrivacy: '/settings/data-privacy', @@ -132,6 +134,11 @@ const router = createBrowserRouter([ Component: Logs, errorElement: , }, + { + path: routes.dns, + Component: CustomDNS, + errorElement: , + }, { path: routes.antiCensorship, Component: AntiCensorship, diff --git a/nym-vpn-app/src/screens/settings/Settings.tsx b/nym-vpn-app/src/screens/settings/Settings.tsx index 474d1fd0f9..9057890a63 100644 --- a/nym-vpn-app/src/screens/settings/Settings.tsx +++ b/nym-vpn-app/src/screens/settings/Settings.tsx @@ -82,6 +82,12 @@ function Settings() { onClick: handleAllowLan, trailing: , }, + { + title: t('dns.title'), + leadingIcon: 'dns', + onClick: () => navigate(routes.dns), + trailing: , + }, { title: t('anti-censorship.title', { ns: 'settings' }), leadingIcon: 'campaign', diff --git a/nym-vpn-app/src/screens/settings/anti-censorship/AntiCensorship.tsx b/nym-vpn-app/src/screens/settings/anti-censorship/AntiCensorship.tsx index 984bbf6803..e89fcd0644 100644 --- a/nym-vpn-app/src/screens/settings/anti-censorship/AntiCensorship.tsx +++ b/nym-vpn-app/src/screens/settings/anti-censorship/AntiCensorship.tsx @@ -64,31 +64,6 @@ function AntiCensorship() {
{t('anti-censorship.intro')}
- { - /* TODO */ - }} - /> - } - > -
-

- {t('anti-censorship.amneziawg.content')} -

- -
-
{backendFlags.quic && ( )} + { + /* TODO */ + }} + /> + } + > +
+

+ {t('anti-censorship.amneziawg.content')} +

+ +
+
void; + onConfirm: () => Promise; +}) { + const [isApplyingDns, setIsApplyingDns] = useState(false); + + const handleConfirm = async () => { + setIsApplyingDns(true); + try { + await onConfirm(); + } finally { + setIsApplyingDns(false); + } + }; + + return ( + +
+ + + + Apply DNS changes? + +
+

+ Custom DNS settings updated. Reconnect to apply changes.

{' '} + Your data stays protected during reconnection. +

+
+ + +
+
+ ); +} diff --git a/nym-vpn-app/src/screens/settings/custom-dns/CustomDNS.tsx b/nym-vpn-app/src/screens/settings/custom-dns/CustomDNS.tsx new file mode 100644 index 0000000000..d76925b70d --- /dev/null +++ b/nym-vpn-app/src/screens/settings/custom-dns/CustomDNS.tsx @@ -0,0 +1,94 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { motion } from 'motion/react'; +import { + CardSwitch, + Link, + PageAnim, + SettingsMenuCard, + SettingsMenuCardBig, +} from '../../../ui'; +import { CustomDnsHelpUrl } from '../../../constants'; +import useCustomDns from '../../../hooks/useCustomDns'; +import { CustomDnsServers } from './CustomDnsServers'; +import { DefaultDnsServers } from './DefaultDnsServers'; + +function CustomDNS() { + const { t } = useTranslation('settings'); + const { + enabled: customDnsEnabled, + toggle: toggleCustomDns, + setCustomDns, + } = useCustomDns(); + + const [dnsEnabledLocal, setDnsEnabledLocal] = useState( + () => customDnsEnabled, + ); + + const description = dnsEnabledLocal + ? t('dns.details.on.description') + : t('dns.details.off.description'); + + const handleDnsSwitchChange = async () => { + const newState = !dnsEnabledLocal; + // User can switch off immediately. Switching on will show a confirmation dialog before applying changes. + if (newState === false) { + await toggleCustomDns(false); + } + setDnsEnabledLocal(newState); + }; + + const applyChanges = async (dnsList: string[]) => { + await toggleCustomDns(dnsEnabledLocal); + await setCustomDns(dnsList); + }; + + return ( + + + } + > +
+

+ {description} +

+ + {dnsEnabledLocal ? ( + + ) : ( + + )} +
+ +
+ + {dnsEnabledLocal && ( + + + + )} +
+ ); +} + +export default CustomDNS; diff --git a/nym-vpn-app/src/screens/settings/custom-dns/CustomDnsServers.tsx b/nym-vpn-app/src/screens/settings/custom-dns/CustomDnsServers.tsx new file mode 100644 index 0000000000..3d2005312f --- /dev/null +++ b/nym-vpn-app/src/screens/settings/custom-dns/CustomDnsServers.tsx @@ -0,0 +1,159 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, TextInput } from '../../../ui'; +import DraggableList from '../../../ui/DraggableList'; +import { ipv4Regex, ipv6Regex } from '../../../utils'; +import { useInAppNotify, useMainState } from '../../../contexts/index'; +import useCustomDns from '../../../hooks/useCustomDns'; +import { DnsItem, DnsItemContent } from './DnsItemContent'; +import { ConfirmationDialog } from './ConfirmationDialog'; + +const MAX_DNS_SERVERS = 5; + +export function CustomDnsServers({ + onApplyDns, +}: { + onApplyDns: (dnsList: string[]) => Promise; +}) { + const { state } = useMainState(); + const { t } = useTranslation('settings'); + const { push } = useInAppNotify(); + const { customDns: customDnsList } = useCustomDns(); + + const [dnsList, setDnsList] = useState(() => + customDnsList.map((dns) => ({ id: dns, dns: dns })), + ); + const [inputValue, setInputValue] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const [isConfirmationDialogOpen, setIsConfirmationDialogOpen] = + useState(false); + const [isApplyingDns, setIsApplyingDns] = useState(false); + + const handleAddDns = () => { + const inputValueTrimmed = inputValue.trim(); + const containsDuplicate = dnsList.some( + (item) => item.dns === inputValueTrimmed, + ); + const isValid = + ipv4Regex.test(inputValueTrimmed) || ipv6Regex.test(inputValueTrimmed); + + if (inputValueTrimmed === '') return; + + if (containsDuplicate) { + setErrorMessage('Duplicate DNS address'); + return; + } + + if (!isValid) { + setErrorMessage('Invalid DNS address format'); + return; + } + + setDnsList((prev) => [ + ...prev, + { id: inputValueTrimmed, dns: inputValueTrimmed }, + ]); + setInputValue(''); + }; + + const applyDns = async () => { + setIsApplyingDns(true); + await onApplyDns(dnsList.map((item) => item.dns)); + setIsApplyingDns(false); + }; + + const handleTextInputChange = (value: string) => { + setInputValue(value); + setErrorMessage(''); + }; + + const handleDeleteDns = (dns: string) => { + setDnsList((prev) => prev.filter((d) => d.id !== dns)); + }; + + const handleReorder = (items: DnsItem[]) => { + setDnsList(items); + }; + + const handleApplyButtonClick = async () => { + if (state === 'connected') { + setIsConfirmationDialogOpen(true); + } else { + await applyDns(); + push({ + message: 'DNS changes applied', + close: true, + }); + } + }; + + const handleHandleDialogConfirm = async () => { + await applyDns(); + setIsConfirmationDialogOpen(false); + }; + + return ( + <> + {dnsList.length > 0 && ( +
+

+ Custom DNS servers ({dnsList.length}/{MAX_DNS_SERVERS}) +

+
+ ( + + )} + /> +
+
+ )} + {dnsList.length < MAX_DNS_SERVERS && ( +
+
+ + {errorMessage && ( +

{errorMessage}

+ )} +
+
+ +
+
+ )} + + + setIsConfirmationDialogOpen(false)} + onConfirm={handleHandleDialogConfirm} + /> + + ); +} diff --git a/nym-vpn-app/src/screens/settings/custom-dns/DefaultDnsServers.tsx b/nym-vpn-app/src/screens/settings/custom-dns/DefaultDnsServers.tsx new file mode 100644 index 0000000000..fa9e9b58e8 --- /dev/null +++ b/nym-vpn-app/src/screens/settings/custom-dns/DefaultDnsServers.tsx @@ -0,0 +1,25 @@ +import useCustomDns from '../../../hooks/useCustomDns'; +import { MsIcon } from '../../../ui'; + +export function DefaultDnsServers() { + const { defaultDns } = useCustomDns(); + + return ( +
+

Default DNS servers

+
+ {defaultDns.map((dns) => ( +
+ +

+ {dns} +

+
+ ))} +
+
+ ); +} diff --git a/nym-vpn-app/src/screens/settings/custom-dns/DnsItemContent.tsx b/nym-vpn-app/src/screens/settings/custom-dns/DnsItemContent.tsx new file mode 100644 index 0000000000..81a90d56c3 --- /dev/null +++ b/nym-vpn-app/src/screens/settings/custom-dns/DnsItemContent.tsx @@ -0,0 +1,36 @@ +import { type ReactNode } from 'react'; +import { ButtonIcon, type DraggableListItem } from '../../../ui'; + +export type DnsItem = DraggableListItem & { + dns: string; +}; + +export function DnsItemContent({ + item, + dragHandle, + onDelete, +}: { + item: DnsItem; + dragHandle: ReactNode; + onDelete: (dns: string) => void; +}) { + return ( +
+
+ {dragHandle} +

+ {item.dns} +

+
+ { + onDelete(item.id); + }} + noDefaultSize + className="shrink-0" + /> +
+ ); +} diff --git a/nym-vpn-app/src/screens/settings/custom-dns/index.ts b/nym-vpn-app/src/screens/settings/custom-dns/index.ts new file mode 100644 index 0000000000..412bda5702 --- /dev/null +++ b/nym-vpn-app/src/screens/settings/custom-dns/index.ts @@ -0,0 +1 @@ +export { default as CustomDNS } from './CustomDNS'; diff --git a/nym-vpn-app/src/screens/settings/index.ts b/nym-vpn-app/src/screens/settings/index.ts index 7eebb7e32e..8f508dbd17 100644 --- a/nym-vpn-app/src/screens/settings/index.ts +++ b/nym-vpn-app/src/screens/settings/index.ts @@ -7,4 +7,5 @@ export * from './support'; export * from './dev'; export * from './data-privacy'; export * from './anti-censorship'; +export * from './custom-dns'; export * from './socks5'; diff --git a/nym-vpn-app/src/state/init.ts b/nym-vpn-app/src/state/init.ts index ad9b3b9977..86f24bce9b 100644 --- a/nym-vpn-app/src/state/init.ts +++ b/nym-vpn-app/src/state/init.ts @@ -264,7 +264,18 @@ export async function initSecondBatch( }, }; - let requests: TauriReq[] = [getAutostart]; + const getDefaultDnsRq: TauriReq<() => Promise> = { + name: 'getDefaultDnsRq', + request: () => invoke('get_default_dns'), + onFulfilled: (dns) => { + dispatch({ + type: 'set-default-dns', + dns: dns || [], + }); + }, + }; + + let requests: TauriReq[] = [getAutostart, getDefaultDnsRq]; if (initState.vpnd !== 'down') { requests = [getAccountLinksRq, getNetworkCompatRq, ...requests]; } diff --git a/nym-vpn-app/src/types/app-state.ts b/nym-vpn-app/src/types/app-state.ts index 2da108857e..6c00d721c7 100644 --- a/nym-vpn-app/src/types/app-state.ts +++ b/nym-vpn-app/src/types/app-state.ts @@ -37,6 +37,8 @@ export type InitState = { quic: boolean; noIpv6: boolean; allowLan: boolean; + customDnsEnabled: boolean; + customDns: string[]; }; export type AppState = { @@ -90,4 +92,7 @@ export type AppState = { domainFronting: boolean; // whether the user has seen the streaming optimized label feature alert streamingOptimizedLabelSeen: boolean; + customDnsEnabled: boolean; + customDns: string[]; + defaultDns: string[]; }; diff --git a/nym-vpn-app/src/types/tauri.ts b/nym-vpn-app/src/types/tauri.ts index f9349f9ce1..bf4af8097b 100644 --- a/nym-vpn-app/src/types/tauri.ts +++ b/nym-vpn-app/src/types/tauri.ts @@ -354,6 +354,7 @@ export type VpndConfig = { entryNode: SelectedNode; exitNode: SelectedNode; customDns: Array | null; + enableCustomDns: boolean; allowLan: boolean; disableIpv6: boolean; vpnMode: VpnMode; diff --git a/nym-vpn-app/src/ui/DraggableList.tsx b/nym-vpn-app/src/ui/DraggableList.tsx new file mode 100644 index 0000000000..a64713e2ae --- /dev/null +++ b/nym-vpn-app/src/ui/DraggableList.tsx @@ -0,0 +1,134 @@ +import { ReactNode } from 'react'; +import { + DndContext, + DragEndEvent, + KeyboardSensor, + PointerSensor, + closestCenter, + useSensor, + useSensors, +} from '@dnd-kit/core'; +import { + SortableContext, + arrayMove, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; +import clsx from 'clsx'; +import { CSS } from '@dnd-kit/utilities'; +import MsIcon from './MsIcon'; + +export type DraggableListItem = { + id: string; +}; + +type SortableItemProps = { + item: T; + renderItem: (item: T, dragHandle: ReactNode) => ReactNode; + dragHandleClassName?: string; +}; + +function SortableItem({ + item, + renderItem, + dragHandleClassName, +}: SortableItemProps) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: item.id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + const dragHandle = ( + + ); + + return ( +
+ {renderItem(item, dragHandle)} +
+ ); +} + +export type DraggableListProps = { + items: T[]; + onReorder: (items: T[]) => void; + renderItem: (item: T, dragHandle: ReactNode) => ReactNode; + dragHandleClassName?: string; +}; + +function DraggableList({ + items, + onReorder, + renderItem, + dragHandleClassName, +}: DraggableListProps) { + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + const oldIndex = items.findIndex((item) => item.id === active.id); + const newIndex = items.findIndex((item) => item.id === over.id); + onReorder(arrayMove(items, oldIndex, newIndex)); + } + }; + + return ( + + item.id)} + strategy={verticalListSortingStrategy} + > +
+ {/*
*/} + {items.map((item) => ( + + ))} +
+ + + ); +} + +export default DraggableList; diff --git a/nym-vpn-app/src/ui/SettingsMenuCard.tsx b/nym-vpn-app/src/ui/SettingsMenuCard.tsx index 8718bc576a..d4c0207e0b 100644 --- a/nym-vpn-app/src/ui/SettingsMenuCard.tsx +++ b/nym-vpn-app/src/ui/SettingsMenuCard.tsx @@ -15,7 +15,7 @@ export type SettingsMenuCardProps = { className?: string; style?: CSSProperties; noHoverEffect?: boolean; - color?: 'normal' | 'red'; + color?: 'normal' | 'red' | 'gray'; }; function SettingsMenuCard({ @@ -47,6 +47,11 @@ function SettingsMenuCard({ color === 'red' && !noHoverEffect && 'hover:bg-aphrodisiac/20 dark:hover:bg-aphrodisiac/20', + // gray color + color === 'gray' && 'bg-white dark:bg-mine-shaft', + color === 'gray' && + !noHoverEffect && + 'hover:bg-white/60 dark:hover:bg-mine-shaft/85', 'flex flex-row justify-between items-center gap-4 select-none', 'px-5 rounded-lg min-h-16', description ? 'py-2' : 'py-4', diff --git a/nym-vpn-app/src/ui/TextInput.tsx b/nym-vpn-app/src/ui/TextInput.tsx index 93ca21bd03..cdea37e47e 100644 --- a/nym-vpn-app/src/ui/TextInput.tsx +++ b/nym-vpn-app/src/ui/TextInput.tsx @@ -21,6 +21,7 @@ export type TextInputProps = { leftIcon?: string; readonly?: boolean; clearable?: boolean; + color?: 'default' | 'gray'; }; function TextInput({ @@ -35,6 +36,7 @@ function TextInput({ autoFocus, className, clearable = false, + color = 'default', }: TextInputProps) { const handleChange = (e: React.ChangeEvent) => { onChange(e.target.value); @@ -44,6 +46,15 @@ function TextInput({ onChange(''); }; + const getColorClass = () => { + switch (color) { + case 'default': + return 'bg-faded-lavender dark:bg-ash'; + case 'gray': + return 'bg-white dark:bg-charcoal'; + } + }; + return ( {label} diff --git a/nym-vpn-app/src/ui/TopBar.tsx b/nym-vpn-app/src/ui/TopBar.tsx index 909da32fb9..e05cac42f5 100644 --- a/nym-vpn-app/src/ui/TopBar.tsx +++ b/nym-vpn-app/src/ui/TopBar.tsx @@ -121,6 +121,13 @@ export default function TopBar() { navigate(-1); }, }, + '/settings/dns': { + title: t('dns.topbar-title', { ns: 'settings' }), + leftIcon: 'arrow_back', + handleLeftNav: () => { + navigate(-1); + }, + }, '/settings/anti-censorship': { title: t('anti-censorship.title', { ns: 'settings' }), leftIcon: 'arrow_back', diff --git a/nym-vpn-app/src/ui/index.ts b/nym-vpn-app/src/ui/index.ts index 95dfa3007f..8688702420 100644 --- a/nym-vpn-app/src/ui/index.ts +++ b/nym-vpn-app/src/ui/index.ts @@ -2,6 +2,7 @@ export { default as Button } from './Button'; export { default as ButtonIcon } from './ButtonIcon'; export { default as ButtonText } from './ButtonText'; export { default as Dialog } from './Dialog'; +export { default as DraggableList } from './DraggableList'; export { default as MsIcon } from './MsIcon'; export { default as DaemonDot } from './DaemonDot'; export { default as RadioGroup } from './RadioGroup'; @@ -26,6 +27,7 @@ export * from './Button'; export * from './ButtonIcon'; export * from './ButtonText'; export * from './Dialog'; +export * from './DraggableList'; export * from './FlagIcon'; export * from './MsIcon'; export * from './RadioGroup'; diff --git a/nym-vpn-app/src/utils/index.ts b/nym-vpn-app/src/utils/index.ts new file mode 100644 index 0000000000..916d4db3ec --- /dev/null +++ b/nym-vpn-app/src/utils/index.ts @@ -0,0 +1 @@ +export * from './regex'; diff --git a/nym-vpn-app/src/utils/regex.ts b/nym-vpn-app/src/utils/regex.ts new file mode 100644 index 0000000000..00c7b8812c --- /dev/null +++ b/nym-vpn-app/src/utils/regex.ts @@ -0,0 +1,5 @@ +export const ipv4Regex = + /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/; + +export const ipv6Regex = + /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(([0-9a-fA-F]{1,4}:){1,7}:)|(([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4})|(([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2})|(([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3})|(([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4})|(([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5})|([0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6}))|(:((:[0-9a-fA-F]{1,4}){1,7}|:)))(%.+)?$/; diff --git a/nym-vpn-apple/NymVPN.xcodeproj/project.pbxproj b/nym-vpn-apple/NymVPN.xcodeproj/project.pbxproj index 2a44b3fbfb..2b961693b0 100644 --- a/nym-vpn-apple/NymVPN.xcodeproj/project.pbxproj +++ b/nym-vpn-apple/NymVPN.xcodeproj/project.pbxproj @@ -879,7 +879,7 @@ CODE_SIGN_ENTITLEMENTS = NymMixnetTunnel/NymMixnetTunnel.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 423; + CURRENT_PROJECT_VERSION = 424; DEVELOPMENT_TEAM = VW5DZLFHM5; ENABLE_APP_SANDBOX = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; @@ -895,7 +895,7 @@ "@executable_path/../../Frameworks", ); LIBRARY_SEARCH_PATHS = "$(inherited)"; - MARKETING_VERSION = 2.14.0; + MARKETING_VERSION = 2.15.0; PRODUCT_BUNDLE_IDENTIFIER = "net.nymtech.vpn.network-extension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -917,7 +917,7 @@ CODE_SIGN_ENTITLEMENTS = NymMixnetTunnel/NymMixnetTunnel.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 423; + CURRENT_PROJECT_VERSION = 424; DEVELOPMENT_TEAM = VW5DZLFHM5; ENABLE_APP_SANDBOX = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; @@ -933,7 +933,7 @@ "@executable_path/../../Frameworks", ); LIBRARY_SEARCH_PATHS = "$(inherited)"; - MARKETING_VERSION = 2.14.0; + MARKETING_VERSION = 2.15.0; PRODUCT_BUNDLE_IDENTIFIER = "net.nymtech.vpn.network-extension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1081,7 +1081,7 @@ CODE_SIGN_ENTITLEMENTS = NymVPN/NymVPN.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 423; + CURRENT_PROJECT_VERSION = 424; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = VW5DZLFHM5; ENABLE_APP_SANDBOX = YES; @@ -1098,7 +1098,7 @@ "$(inherited)", "$(PROJECT_DIR)", ); - MARKETING_VERSION = 2.14.0; + MARKETING_VERSION = 2.15.0; PRODUCT_BUNDLE_IDENTIFIER = net.nymtech.vpn; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1122,7 +1122,7 @@ CODE_SIGN_ENTITLEMENTS = NymVPN/NymVPN.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 423; + CURRENT_PROJECT_VERSION = 424; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = VW5DZLFHM5; ENABLE_APP_SANDBOX = YES; @@ -1139,7 +1139,7 @@ "$(inherited)", "$(PROJECT_DIR)", ); - MARKETING_VERSION = 2.14.0; + MARKETING_VERSION = 2.15.0; PRODUCT_BUNDLE_IDENTIFIER = net.nymtech.vpn; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1164,7 +1164,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"NymVPNDaemon/Preview Content\""; DEVELOPMENT_TEAM = VW5DZLFHM5; @@ -1181,7 +1181,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.14.0; + MARKETING_VERSION = 2.15.0; PRODUCT_BUNDLE_IDENTIFIER = net.nymtech.vpn; PRODUCT_NAME = NymVPN; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1202,7 +1202,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"NymVPNDaemon/Preview Content\""; DEVELOPMENT_TEAM = ""; @@ -1220,7 +1220,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.14.0; + MARKETING_VERSION = 2.15.0; PRODUCT_BUNDLE_IDENTIFIER = net.nymtech.vpn; PRODUCT_NAME = NymVPN; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/nym-vpn-core/CHANGELOG.md b/nym-vpn-core/CHANGELOG.md index bc599f92c5..88e45a882b 100644 --- a/nym-vpn-core/CHANGELOG.md +++ b/nym-vpn-core/CHANGELOG.md @@ -7,11 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add custom DNS setting for mobile platforms (https://github.com/nymtech/nym-vpn-client/pull/4106) + ### Fixed - Increase the number of Windows firewall slots (https://github.com/nymtech/nym-vpn-client/pull/4072) - Enable two-hop by default (https://github.com/nymtech/nym-vpn-client/pull/4090) +### Removed + +- CLI: remove legacy call to connect the tunnel (https://github.com/nymtech/nym-vpn-client/pull/4094) + ## [1.20.0] - 2025-12-01 ### Added @@ -27,8 +35,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Avoid connection looping by temporarily blacklisting the entry gateway (https://github.com/nymtech/nym-vpn-client/pull/4047) -### Removed - ## [1.19.0] - 2025-11-19 ### Added diff --git a/nym-vpn-core/crates/nym-vpn-lib-types/src/lib.rs b/nym-vpn-core/crates/nym-vpn-lib-types/src/lib.rs index 0ba631af05..1edf1db060 100644 --- a/nym-vpn-core/crates/nym-vpn-lib-types/src/lib.rs +++ b/nym-vpn-core/crates/nym-vpn-lib-types/src/lib.rs @@ -72,8 +72,8 @@ pub use network::{ SystemConfiguration, SystemMessage, ValidatorDetails, }; pub use rpc_requests::{ - AccountBalanceResponse, AccountCommandResponse, Coin, ConnectArgs, ConnectOptions, - DecentralisedObtainTicketbooksRequest, ListGatewaysOptions, StoreAccountRequest, + AccountBalanceResponse, AccountCommandResponse, Coin, DecentralisedObtainTicketbooksRequest, + ListGatewaysOptions, StoreAccountRequest, }; pub use service::{TargetState, VpnServiceConfig, VpnServiceInfo}; pub use socks5::{HttpRpcSettings, Socks5Settings, Socks5State, Socks5Status}; diff --git a/nym-vpn-core/crates/nym-vpn-lib-types/src/rpc_requests.rs b/nym-vpn-core/crates/nym-vpn-lib-types/src/rpc_requests.rs index bc3834dbad..447a1eb210 100644 --- a/nym-vpn-core/crates/nym-vpn-lib-types/src/rpc_requests.rs +++ b/nym-vpn-core/crates/nym-vpn-lib-types/src/rpc_requests.rs @@ -1,14 +1,12 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use std::net::IpAddr; - #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; #[cfg(feature = "typescript-bindings")] use ts_rs::TS; -use crate::{EntryPoint, ExitPoint, GatewayType, UserAgent}; +use crate::{GatewayType, UserAgent}; #[derive(Debug)] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] @@ -88,25 +86,3 @@ impl From for Coin { pub struct AccountBalanceResponse { pub result: Result, crate::AccountCommandError>, } - -// Deprecated -#[derive(Debug)] -pub struct ConnectArgs { - pub entry: Option, - pub exit: Option, - pub options: ConnectOptions, -} - -// Deprecated -#[derive(Default, Debug, Clone)] -pub struct ConnectOptions { - pub dns: Option, - pub disable_ipv6: bool, - pub enable_two_hop: bool, - pub enable_bridges: bool, - pub netstack: bool, - pub disable_poisson_rate: bool, - pub disable_background_cover_traffic: bool, - pub enable_credentials_mode: bool, - pub user_agent: Option, -} diff --git a/nym-vpn-core/crates/nym-vpn-lib-types/src/service.rs b/nym-vpn-core/crates/nym-vpn-lib-types/src/service.rs index 0a81964130..ad5e5b7f26 100644 --- a/nym-vpn-core/crates/nym-vpn-lib-types/src/service.rs +++ b/nym-vpn-core/crates/nym-vpn-lib-types/src/service.rs @@ -35,7 +35,8 @@ pub struct VpnServiceConfig { pub min_gateway_mixnet_performance: Option, pub min_gateway_vpn_performance: Option, pub residential_exit: bool, - pub custom_dns: Option>, + pub enable_custom_dns: bool, + pub custom_dns: Vec, } impl fmt::Display for VpnServiceConfig { @@ -71,15 +72,13 @@ impl fmt::Display for VpnServiceConfig { writeln!(f, "residential_exit: {}", self.residential_exit)?; writeln!( f, - "custom_dns: {}", + "enable_custom_dns: {}, custom_dns: {}", + self.enable_custom_dns, self.custom_dns - .as_ref() - .map(|dns| dns - .iter() - .map(|ip| ip.to_string()) - .collect::>() - .join(", ")) - .unwrap_or_else(|| "none".to_string()) + .iter() + .map(|ip| ip.to_string()) + .collect::>() + .join(", ") )?; Ok(()) } @@ -101,7 +100,8 @@ impl Default for VpnServiceConfig { min_gateway_mixnet_performance: None, min_gateway_vpn_performance: None, residential_exit: false, - custom_dns: None, + enable_custom_dns: false, + custom_dns: vec![], } } } diff --git a/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/lib.rs b/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/lib.rs index 1a729a5fff..85a7ff421b 100644 --- a/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/lib.rs +++ b/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/lib.rs @@ -75,14 +75,15 @@ use nym_vpn_store::keys::wireguard::WireguardKeysDb; use sentry::ClientInitGuard; use tokio::{runtime::Runtime, sync::Mutex}; -use self::error::VpnError; -use crate::gateway_cache::UniffiGatewayCacheHandle; -use account::AccountControllerHandle; use nym_vpn_lib_types::{ AccountControllerState, EntryPoint, ExitPoint, Gateway, GatewayType, Network, NetworkCompatibility, ParsedAccountLinks, RegisterAccountResponse, SystemMessage, TunnelEvent, UserAgent, }; + +use account::AccountControllerHandle; +use error::VpnError; +use gateway_cache::UniffiGatewayCacheHandle; use offline_monitor::OfflineMonitorHandle; use state_machine::StateMachineHandle; use stats::StatisticsControllerHandle; @@ -531,6 +532,9 @@ pub struct VPNConfig { pub enable_two_hop: bool, pub enable_bridges: bool, pub residential_exit: bool, + /// Custom DNS used when set. + /// Leave empty to use default DNS servers. + pub custom_dns: Vec, #[cfg(target_os = "android")] pub tun_provider: Arc, #[cfg(target_os = "ios")] diff --git a/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/state_machine.rs b/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/state_machine.rs index 8e51e846e9..d6fce69aaf 100644 --- a/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/state_machine.rs +++ b/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/state_machine.rs @@ -75,6 +75,11 @@ pub(super) async fn start_state_machine( TunnelType::Mixnet }; + let dns = if config.custom_dns.is_empty() { + DnsOptions::default() + } else { + DnsOptions::Custom(config.custom_dns) + }; let user_agent = UserAgent::from(config.user_agent); let entry_point = config.entry_gateway; @@ -110,7 +115,7 @@ pub(super) async fn start_state_machine( mixnet_client_config: None, entry_point: Box::new(entry_point), exit_point: Box::new(exit_point), - dns: DnsOptions::default(), + dns, }; let tunnel_constants = TunnelConstants::default(); diff --git a/nym-vpn-core/crates/nym-vpn-proto/proto/nym_vpn_service.proto b/nym-vpn-core/crates/nym-vpn-proto/proto/nym_vpn_service.proto index d8a4eebab9..6d7262fe4f 100644 --- a/nym-vpn-core/crates/nym-vpn-proto/proto/nym_vpn_service.proto +++ b/nym-vpn-core/crates/nym-vpn-proto/proto/nym_vpn_service.proto @@ -240,20 +240,6 @@ message Threshold { uint32 min_performance = 1; } -message ConnectRequest { - EntryNode entry = 1; - ExitNode exit = 2; - Dns dns = 3; - bool disable_ipv6 = 14; - bool enable_two_hop = 5; - bool enable_bridges = 21; - bool netstack = 13; - bool disable_poisson_rate = 6; - bool disable_background_cover_traffic = 7; - bool enable_credentials_mode = 8; - UserAgent user_agent = 12; -} - message VpnServiceConfig { EntryNode entry_point = 1; ExitNode exit_point = 2; @@ -268,6 +254,7 @@ message VpnServiceConfig { optional uint32 min_mixnode_performance = 8; optional uint32 min_gateway_mixnet_performance = 9; optional uint32 min_gateway_vpn_performance = 10; + bool enable_custom_dns = 17; IpAddrList custom_dns = 16; } @@ -851,6 +838,7 @@ service NymVpnService { rpc SetNetstack (google.protobuf.BoolValue) returns (google.protobuf.Empty) {} rpc SetAllowLan (google.protobuf.BoolValue) returns (google.protobuf.Empty) {} rpc SetResidentialExit (google.protobuf.BoolValue) returns (google.protobuf.Empty) {} + rpc SetEnableCustomDns (google.protobuf.BoolValue) returns (google.protobuf.Empty) {} rpc SetCustomDns (IpAddrList) returns (google.protobuf.Empty) {} // Set the network. This requires a restart to take effect @@ -868,11 +856,8 @@ service NymVpnService { // Get the default DNS used by the daemon rpc GetDefaultDns (google.protobuf.Empty) returns (IpAddrList) {} - // Connect the tunnel (deprecated; use ConnectTunnelV2 instead). - rpc ConnectTunnel (ConnectRequest) returns (google.protobuf.Empty) {} - - // Connect the tunnel (Configuration is owned by daemon) - rpc ConnectTunnelV2 (google.protobuf.Empty) returns (google.protobuf.BoolValue) {} + // Connect the tunnel + rpc ConnectTunnel (google.protobuf.Empty) returns (google.protobuf.BoolValue) {} // Reconnect the tunnel if it had been established. rpc ReconnectTunnel (google.protobuf.Empty) returns (google.protobuf.BoolValue) {} diff --git a/nym-vpn-core/crates/nym-vpn-proto/src/conversions/service.rs b/nym-vpn-core/crates/nym-vpn-proto/src/conversions/service.rs index 0afb5a2d4e..56dd48f84e 100644 --- a/nym-vpn-core/crates/nym-vpn-proto/src/conversions/service.rs +++ b/nym-vpn-core/crates/nym-vpn-proto/src/conversions/service.rs @@ -20,10 +20,10 @@ impl TryFrom for nym_vpn_lib_types::VpnServiceConfig { .transpose()? .ok_or(ConversionError::NoValueSet("VpnServiceConfig.exit_point"))?; - let custom_dns: Option> = value - .custom_dns - .map(proto::IpAddrList::try_into) - .transpose()?; + let custom_dns: Vec = match value.custom_dns { + Some(ip_addr_list) => ip_addr_list.try_into()?, + None => vec![], + }; let config = nym_vpn_lib_types::VpnServiceConfig { entry_point, @@ -39,6 +39,7 @@ impl TryFrom for nym_vpn_lib_types::VpnServiceConfig { min_gateway_mixnet_performance: value.min_gateway_mixnet_performance.map(|u| u as u8), min_gateway_vpn_performance: value.min_gateway_vpn_performance.map(|u| u as u8), residential_exit: value.residential_exit, + enable_custom_dns: value.enable_custom_dns, custom_dns, }; Ok(config) @@ -49,7 +50,7 @@ impl From for proto::VpnServiceConfig { fn from(value: nym_vpn_lib_types::VpnServiceConfig) -> Self { let entry_point = Some(proto::EntryNode::from(value.entry_point)); let exit_point = Some(proto::ExitNode::from(value.exit_point)); - let custom_dns = value.custom_dns.map(proto::IpAddrList::from); + let custom_dns = Some(proto::IpAddrList::from(value.custom_dns)); proto::VpnServiceConfig { entry_point, @@ -65,6 +66,7 @@ impl From for proto::VpnServiceConfig { min_gateway_mixnet_performance: value.min_gateway_mixnet_performance.map(|u| u as u32), min_gateway_vpn_performance: value.min_gateway_vpn_performance.map(|u| u as u32), residential_exit: value.residential_exit, + enable_custom_dns: value.enable_custom_dns, custom_dns, } } diff --git a/nym-vpn-core/crates/nym-vpn-proto/src/conversions/vpnd.rs b/nym-vpn-core/crates/nym-vpn-proto/src/conversions/vpnd.rs index 9b45e0b1b3..4b82c2e091 100644 --- a/nym-vpn-core/crates/nym-vpn-proto/src/conversions/vpnd.rs +++ b/nym-vpn-core/crates/nym-vpn-proto/src/conversions/vpnd.rs @@ -10,9 +10,9 @@ use std::{ use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use nym_vpn_lib_types::{ - AccountCommandResponse, ApiUrl, BridgeInformation, BridgeParameters, ConnectArgs, - ConnectOptions, GatewayType, ListGatewaysOptions, LogPath, NymNetworkDetails, NymVpnNetwork, - Performance, QuicClientOptions, StoreAccountRequest, SystemMessage, UserAgent, VpnServiceInfo, + AccountCommandResponse, ApiUrl, BridgeInformation, BridgeParameters, GatewayType, + ListGatewaysOptions, LogPath, NymNetworkDetails, NymVpnNetwork, Performance, QuicClientOptions, + StoreAccountRequest, SystemMessage, UserAgent, VpnServiceInfo, }; use crate::{conversions::ConversionError, proto}; @@ -437,79 +437,6 @@ impl From for SystemMessage { } } -impl TryFrom for ConnectArgs { - type Error = ConversionError; - - fn try_from(value: proto::ConnectRequest) -> Result { - let entry = value - .entry - .clone() // todo: prevent clone() - .map(nym_vpn_lib_types::EntryPoint::try_from) - .transpose()?; - let exit = value - .exit - .clone() // todo: prevent clone() - .map(nym_vpn_lib_types::ExitPoint::try_from) - .transpose()?; - let options = ConnectOptions::try_from(value)?; - Ok(ConnectArgs { - entry, - exit, - options, - }) - } -} - -impl TryFrom for proto::ConnectRequest { - type Error = ConversionError; - - fn try_from(value: ConnectArgs) -> Result { - let entry = value.entry.map(proto::EntryNode::from); - let exit = value.exit.map(proto::ExitNode::from); - Ok(Self { - dns: value.options.dns.map(|ip| proto::Dns { - ip: Some(ip.to_string()), - }), - disable_ipv6: value.options.disable_ipv6, - enable_two_hop: value.options.enable_two_hop, - enable_bridges: value.options.enable_bridges, - netstack: value.options.netstack, - disable_poisson_rate: value.options.disable_poisson_rate, - disable_background_cover_traffic: value.options.disable_background_cover_traffic, - enable_credentials_mode: value.options.enable_credentials_mode, - user_agent: value.options.user_agent.map(proto::UserAgent::from), - entry, - exit, - }) - } -} - -impl TryFrom for ConnectOptions { - type Error = ConversionError; - - fn try_from(value: proto::ConnectRequest) -> Result { - let dns = match value.dns.and_then(|dns| dns.ip) { - Some(ip) => Some( - ip.parse::() - .map_err(|e| ConversionError::ParseAddr("ConnectRequest.dns", e))?, - ), - None => None, - }; - - Ok(Self { - dns, - disable_ipv6: value.disable_ipv6, - enable_two_hop: value.enable_two_hop, - enable_bridges: value.enable_bridges, - netstack: value.netstack, - disable_poisson_rate: value.disable_poisson_rate, - disable_background_cover_traffic: value.disable_background_cover_traffic, - enable_credentials_mode: value.enable_credentials_mode, - user_agent: value.user_agent.map(UserAgent::from), - }) - } -} - impl TryFrom for ListGatewaysOptions { type Error = ConversionError; diff --git a/nym-vpn-core/crates/nym-vpn-proto/src/rpc_client.rs b/nym-vpn-core/crates/nym-vpn-proto/src/rpc_client.rs index fc52fc8b1a..15ba41c9d6 100644 --- a/nym-vpn-core/crates/nym-vpn-proto/src/rpc_client.rs +++ b/nym-vpn-core/crates/nym-vpn-proto/src/rpc_client.rs @@ -3,7 +3,7 @@ use nym_vpn_lib_types::{ AccountBalanceResponse, AccountCommandResponse, AccountControllerState, AvailableTickets, - ConnectArgs, EntryPoint, ExitPoint, FeatureFlags, Gateway, GatewayFilters, HttpRpcSettings, + EntryPoint, ExitPoint, FeatureFlags, Gateway, GatewayFilters, HttpRpcSettings, ListGatewaysOptions, LogPath, NetworkCompatibility, NymVpnDevice, NymVpnUsage, ParsedAccountLinks, Socks5Settings, Socks5Status, StoreAccountRequest, SystemMessage, TunnelEvent, TunnelState, VpnServiceConfig, VpnServiceInfo, @@ -132,6 +132,15 @@ impl RpcClient { Ok(()) } + pub async fn set_enable_custom_dns(&mut self, enable: bool) -> Result<()> { + self.0 + .set_enable_custom_dns(enable) + .await + .map_err(Error::Rpc)? + .into_inner(); + Ok(()) + } + pub async fn set_custom_dns(&mut self, ips: Vec) -> Result<()> { let request: proto::IpAddrList = ips .into_iter() @@ -208,19 +217,9 @@ impl RpcClient { Ok(ip_vec) } - pub async fn connect_tunnel(&mut self, request: ConnectArgs) -> Result<()> { - let request = proto::ConnectRequest::try_from(request).map_err(Error::InvalidRequest)?; - - self.0 - .connect_tunnel(request) - .await - .map(|v| v.into_inner()) - .map_err(Error::Rpc) - } - - pub async fn connect_tunnel_v2(&mut self) -> Result { + pub async fn connect_tunnel(&mut self) -> Result { self.0 - .connect_tunnel_v2(()) + .connect_tunnel(()) .await .map(|v| v.into_inner()) .map_err(Error::Rpc) diff --git a/nym-vpn-core/crates/nym-vpn-rpc-uniffi/src/lib.rs b/nym-vpn-core/crates/nym-vpn-rpc-uniffi/src/lib.rs index 873d8f0abc..79f58ef18a 100644 --- a/nym-vpn-core/crates/nym-vpn-rpc-uniffi/src/lib.rs +++ b/nym-vpn-core/crates/nym-vpn-rpc-uniffi/src/lib.rs @@ -82,6 +82,11 @@ impl RpcClient { Ok(()) } + pub async fn set_enable_custom_dns(&self, enable: bool) -> Result<()> { + self.inner.clone().set_enable_custom_dns(enable).await?; + Ok(()) + } + pub async fn set_custom_dns(&self, dns_servers: Vec) -> Result<()> { self.inner.clone().set_custom_dns(dns_servers).await?; Ok(()) @@ -112,7 +117,7 @@ impl RpcClient { } pub async fn connect_tunnel(&self) -> Result<()> { - self.inner.clone().connect_tunnel_v2().await?; + self.inner.clone().connect_tunnel().await?; Ok(()) } diff --git a/nym-vpn-core/crates/nym-vpnc/src/commands/dns.rs b/nym-vpn-core/crates/nym-vpnc/src/commands/dns.rs index d3f938dcf8..50e81cee29 100644 --- a/nym-vpn-core/crates/nym-vpnc/src/commands/dns.rs +++ b/nym-vpn-core/crates/nym-vpnc/src/commands/dns.rs @@ -10,6 +10,12 @@ pub enum Command { /// Get Custom DNS servers Get, + /// Enable Custom DNS + Enable, + + /// Disable Custom DNS + Disable, + /// Set Custom DNS servers (space separated) Set { dns_servers: Vec }, @@ -26,18 +32,29 @@ impl Command { Command::Get => { let config = rpc_client.get_config().await?; println!( - "Custom DNS: {}", + "Custom DNS: {} [{}]", + if config.enable_custom_dns { + "Enabled" + } else { + "Disabled" + }, config .custom_dns - .map(|dns| dns - .iter() - .map(|ip| ip.to_string()) - .collect::>() - .join(" ")) - .unwrap_or_else(|| "not set".to_string()) + .iter() + .map(|ip| ip.to_string()) + .collect::>() + .join(" ") ); Ok(()) } + Command::Enable => { + rpc_client.set_enable_custom_dns(true).await?; + Ok(()) + } + Command::Disable => { + rpc_client.set_enable_custom_dns(false).await?; + Ok(()) + } Command::Set { dns_servers } => { let ip_addr_list = dns_servers .iter() @@ -50,6 +67,7 @@ impl Command { Ok(()) } Command::Clear => { + // You can also use `Set`, and specify no servers, but this is clearer. rpc_client.set_custom_dns(vec![]).await?; Ok(()) } diff --git a/nym-vpn-core/crates/nym-vpnc/src/commands/legacy.rs b/nym-vpn-core/crates/nym-vpnc/src/commands/legacy.rs deleted file mode 100644 index 9080c8122b..0000000000 --- a/nym-vpn-core/crates/nym-vpnc/src/commands/legacy.rs +++ /dev/null @@ -1,471 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use std::net::IpAddr; - -use anyhow::{Result, anyhow}; - -use nym_vpn_lib_types::{ - ConnectArgs as DaemonConnectArgs, ConnectOptions, EntryPoint, ExitPoint, NodeIdentity, - Recipient, StoreAccountRequest, -}; -use nym_vpn_proto::rpc_client::RpcClient; - -#[derive(Debug, clap::Subcommand)] -pub enum Command { - /// Connect to the Nym network (deprecated, use instead: nym-vpnc connect-v2) - /// Individual tunnel parameters are configured separately. Learn more by running: - /// - nym-vpnc tunnel --help - /// - nym-vpnc gateway --help - #[clap(verbatim_doc_comment)] - Connect(Box), - - /// Set the network to be used. This requires a restart of the daemon (`nym-vpnd`) (deprecated, use instead: nym-vpnc network set ) - SetNetwork(SetNetworkArgs), - - /// Store the account recovery phrase. (deprecated, use instead: nym-vpnc account set ) - StoreAccount(StoreAccountArgs), - - /// Check if the account is stored. (deprecated, use instead: nym-vpnc account get) - IsAccountStored, - - /// Forget the stored account. This removes the stores recovery phrase, device and mixnet keys, - /// stored local credentials, etc. (deprecated, use instead: nym-vpnc account forget) - ForgetAccount, - - /// Get the account ID. (deprecated, use instead: nym-vpnc account get) - GetAccountId, - - /// Get current account state. (deprecated, use instead: nym-vpnc account get) - GetAccountState, - - /// Get URLs for managing your nym-vpn account. (deprecated, use instead: nym-vpnc account links --locale ) - GetAccountLinks(GetAccountLinksArgs), - - /// Get the device ID. (deprecated, use instead: nym-vpnc device get) - GetDeviceId, - - /// Internal commands for development and debugging. (deprecated) - #[clap(subcommand, hide = true)] - Internal(Internal), -} - -#[derive(Debug, clap::Subcommand)] -pub enum Internal { - /// Get the list of system messages provided by the nym-vpn-api. - GetSystemMessages, - - /// Get the list of feature flags provided by the nym-vpn-api. - GetFeatureFlags, - - /// Manually trigger an account sync with the nym-vpn-api. - SyncAccountState, - - /// Get the account usage from the nym-vpn-api. - GetAccountUsage, - - /// Manually reset the device identity. A seed can be provided as a way to generate a stable - /// identity for testing. - ResetDeviceIdentity(ResetDeviceIdentityArgs), - - /// Get the devices associated with the account. - GetDevices, - - /// Get the active devices associated with the account. - GetActiveDevices, - - /// List the available zknym ticketbooks in the local credential store. - GetAvailableTickets, -} - -impl Command { - pub async fn execute(self, rpc_client: RpcClient) -> Result<()> { - println!("This call is deprecated and going to be removed soon."); - - match self { - Command::Connect(connect_args) => connect(rpc_client, *connect_args).await, - Command::SetNetwork(args) => set_network(rpc_client, args).await, - Command::StoreAccount(store_args) => store_account(rpc_client, store_args).await, - Command::IsAccountStored => is_account_stored(rpc_client).await, - Command::ForgetAccount => forget_account(rpc_client).await, - Command::GetAccountId => get_account_id(rpc_client).await, - Command::GetAccountLinks(args) => get_account_links(rpc_client, args).await, - Command::GetAccountState => get_account_state(rpc_client).await, - Command::GetDeviceId => get_device_id(rpc_client).await, - Command::Internal(internal) => match internal { - Internal::GetSystemMessages => get_system_messages(rpc_client).await, - Internal::GetFeatureFlags => get_feature_flags(rpc_client).await, - Internal::SyncAccountState => refresh_account_state(rpc_client).await, - Internal::GetAccountUsage => get_account_usage(rpc_client).await, - Internal::ResetDeviceIdentity(args) => { - reset_device_identity(rpc_client, args).await - } - Internal::GetDevices => get_devices(rpc_client).await, - Internal::GetActiveDevices => get_active_devices(rpc_client).await, - Internal::GetAvailableTickets => get_available_tickets(rpc_client).await, - }, - } - } -} - -#[derive(Debug, clap::Args)] -pub struct ConnectArgs { - #[command(flatten)] - pub entry: LegacyCliEntry, - - #[command(flatten)] - pub exit: LegacyCliExit, - - /// Set the IP address of the DNS server to use. - #[arg(long)] - pub dns: Option, - - /// Disable IPv6 support - #[arg(long)] - pub disable_ipv6: bool, - - /// Enable two-hop wireguard traffic. This means that traffic jumps directly from entry gateway - /// to exit gateway using Wireguard protocol. - #[arg(long)] - pub enable_two_hop: bool, - - /// Enable Circumvention Transport (CT) wrapping for the connection to the entry gateway in two - /// hop wireguard mode. - #[arg(long = "enable-ct", requires = "enable_two_hop")] - pub circumvention_transports: bool, - - /// Blocks until the connection is established or failed - #[arg(short, long)] - pub wait: bool, - - /// Use netstack based implementation for two-hop wireguard. - #[arg(long, requires = "enable_two_hop")] - pub netstack: bool, - - /// Disable Poisson process rate limiting of outbound traffic. - #[arg(long, hide = true)] - pub disable_poisson_rate: bool, - - /// Disable constant rate background loop cover traffic. - #[arg(long, hide = true)] - pub disable_background_cover_traffic: bool, - - /// Enable credentials mode. - #[arg(long)] - pub enable_credentials_mode: bool, -} - -impl ConnectArgs { - pub fn entry_point(&self) -> Result> { - self.entry.entry_point() - } - - pub fn exit_point(&self) -> Result> { - self.exit.exit_point() - } -} - -#[derive(Debug, clap::Args)] -#[group(multiple = false)] -pub struct LegacyCliEntry { - /// Mixnet public ID of the entry gateway. - #[arg(long, alias = "entry-gateway-id")] - pub entry_id: Option, - - /// Auto-select entry gateway by country ISO. - #[arg(long, alias = "entry-gateway-country")] - pub entry_country: Option, - - /// Auto-select entry gateway randomly. - #[arg(long, alias = "entry-gateway-random")] - pub entry_random: bool, -} - -impl LegacyCliEntry { - pub fn entry_point(&self) -> Result> { - if let Some(ref entry_gateway_id) = self.entry_id { - Ok(Some(EntryPoint::Gateway { - identity: NodeIdentity::from_base58_string(entry_gateway_id) - .map_err(|_| anyhow!("Failed to parse gateway id"))?, - })) - } else if let Some(ref entry_gateway_country) = self.entry_country { - Ok(Some(EntryPoint::Country { - two_letter_iso_country_code: entry_gateway_country.alpha2.to_string(), - })) - } else if self.entry_random { - Ok(Some(EntryPoint::Random)) - } else { - Ok(None) - } - } -} - -#[derive(Debug, clap::Args)] -#[group(multiple = false)] -pub struct LegacyCliExit { - /// Mixnet recipient address of the IPR connecting to, if specified directly. This is only - /// useful when connecting to standalone IPRs. - #[clap(long, hide = true, alias = "exit-router-address")] - pub exit_ipr_address: Option, - - /// Mixnet public ID of the exit gateway. - #[clap(long, alias = "exit-gateway-id")] - pub exit_id: Option, - - /// Auto-select exit gateway by country ISO. - #[clap(long, alias = "exit-gateway-country")] - pub exit_country: Option, - - /// Auto-select exit gateway by region. - #[clap(long, alias = "exit-gateway-region")] - pub exit_region: Option, - - /// Auto-select exit gateway randomly. - #[clap(long, alias = "exit-gateway-random")] - pub exit_random: bool, -} - -impl LegacyCliExit { - pub fn exit_point(&self) -> Result> { - if let Some(ref exit_router_address) = self.exit_ipr_address { - Ok(Some(ExitPoint::Address { - address: Box::new( - Recipient::try_from_base58_string(exit_router_address) - .map_err(|_| anyhow!("Failed to parse exit node address"))?, - ), - })) - } else if let Some(ref exit_router_id) = self.exit_id { - Ok(Some(ExitPoint::Gateway { - identity: NodeIdentity::from_base58_string(exit_router_id.clone()) - .map_err(|_| anyhow!("Failed to parse gateway id"))?, - })) - } else if let Some(ref exit_gateway_country) = self.exit_country { - Ok(Some(ExitPoint::Country { - two_letter_iso_country_code: exit_gateway_country.alpha2.to_string(), - })) - } else if let Some(ref exit_gateway_region) = self.exit_region { - Ok(Some(ExitPoint::Region { - region: exit_gateway_region.to_string(), - })) - } else if self.exit_random { - Ok(Some(ExitPoint::Random)) - } else { - Ok(None) - } - } -} - -impl TryFrom for ExitPoint { - type Error = anyhow::Error; - - fn try_from(value: LegacyCliExit) -> std::result::Result { - if let Some(ref exit_router_address) = value.exit_ipr_address { - Ok(ExitPoint::Address { - address: Box::new( - Recipient::try_from_base58_string(exit_router_address) - .map_err(|_| anyhow!("Failed to parse exit node address"))?, - ), - }) - } else if let Some(ref exit_router_id) = value.exit_id { - Ok(ExitPoint::Gateway { - identity: NodeIdentity::from_base58_string(exit_router_id.clone()) - .map_err(|_| anyhow!("Failed to parse gateway id"))?, - }) - } else if let Some(ref exit_gateway_country) = value.exit_country { - Ok(ExitPoint::Country { - two_letter_iso_country_code: exit_gateway_country.alpha2.to_string(), - }) - } else if let Some(ref exit_gateway_region) = value.exit_region { - Ok(ExitPoint::Region { - region: exit_gateway_region.to_string(), - }) - } else if value.exit_random { - Ok(ExitPoint::Random) - } else { - Err(anyhow!("Invalid Exit Point value")) - } - } -} - -#[derive(Debug, clap::Args)] -pub struct SetNetworkArgs { - /// The network to be set. - pub network: String, -} - -#[derive(Debug, clap::Args)] -pub struct StoreAccountArgs { - /// The account mnemonic to be stored. - #[arg(long)] - pub mnemonic: String, -} - -#[derive(Debug, clap::Args)] -pub struct GetAccountLinksArgs { - /// The locale to be used. - #[arg(long)] - pub locale: String, -} - -#[derive(Debug, clap::Args)] -pub struct ResetDeviceIdentityArgs { - /// Reset the device identity using the given seed. - #[arg(long)] - pub seed: Option, -} - -#[derive(Debug, clap::Args)] -pub struct GetZkNymByIdArgs { - /// The ID of the ZK Nym to fetch. - #[arg(short, long)] - pub id: String, -} - -#[derive(Debug, clap::Args)] -pub struct ConfirmZkNymDownloadedArgs { - /// The ID of the ZK Nym to confirm. - #[arg(short, long)] - pub id: String, -} - -async fn connect(mut rpc_client: RpcClient, connect_args: ConnectArgs) -> Result<()> { - let options = DaemonConnectArgs { - entry: connect_args.entry_point()?, - exit: connect_args.exit_point()?, - options: ConnectOptions { - dns: connect_args.dns, - disable_ipv6: connect_args.disable_ipv6, - enable_two_hop: connect_args.enable_two_hop, - enable_bridges: connect_args.circumvention_transports, - netstack: connect_args.netstack, - disable_poisson_rate: connect_args.disable_poisson_rate, - disable_background_cover_traffic: connect_args.disable_background_cover_traffic, - enable_credentials_mode: connect_args.enable_credentials_mode, - user_agent: None, - }, - }; - - rpc_client.connect_tunnel(options).await?; - - if connect_args.wait { - println!("Waiting until connected or failed"); - crate::wait_until_connected(rpc_client).await - } else { - Ok(()) - } -} - -async fn get_device_id(mut rpc_client: RpcClient) -> Result<()> { - let response = rpc_client.get_device_identity().await?; - println!("{response:#?}"); - Ok(()) -} - -async fn get_devices(mut rpc_client: RpcClient) -> Result<()> { - let response = rpc_client.get_devices().await?; - println!("{response:#?}"); - Ok(()) -} - -async fn get_active_devices(mut rpc_client: RpcClient) -> Result<()> { - let response = rpc_client.get_active_devices().await?; - println!("{response:#?}"); - Ok(()) -} - -async fn set_network(mut rpc_client: RpcClient, args: SetNetworkArgs) -> Result<()> { - rpc_client.set_network(args.network).await?; - Ok(()) -} - -async fn get_system_messages(mut rpc_client: RpcClient) -> Result<()> { - let response = rpc_client.get_system_messages().await?; - println!("{response:#?}"); - Ok(()) -} - -async fn get_feature_flags(mut rpc_client: RpcClient) -> Result<()> { - let response = rpc_client.get_feature_flags().await?; - println!("{response:#?}"); - Ok(()) -} - -async fn store_account(mut rpc_client: RpcClient, store_args: StoreAccountArgs) -> Result<()> { - let request = StoreAccountRequest::Vpn { - mnemonic: store_args.mnemonic, - }; - let response = rpc_client.store_account(request).await?; - - if let Some(err) = response.error { - println!("Error: {err}"); - } else { - println!("Account recovery phrase stored"); - } - - Ok(()) -} - -async fn refresh_account_state(mut rpc_client: RpcClient) -> Result<()> { - rpc_client.refresh_account_state().await?; - Ok(()) -} - -async fn is_account_stored(mut rpc_client: RpcClient) -> Result<()> { - let is_stored = rpc_client.is_account_stored().await?; - if is_stored { - println!("Account is stored"); - } else { - println!("No account is stored"); - } - Ok(()) -} - -async fn get_account_usage(mut rpc_client: RpcClient) -> Result<()> { - let response = rpc_client.get_account_usage().await?; - println!("{response:#?}"); - Ok(()) -} - -async fn forget_account(mut rpc_client: RpcClient) -> Result<()> { - let response = rpc_client.forget_account().await?; - if let Some(err) = response.error { - println!("Error: {err}"); - } else { - println!("Account forgotten successfully"); - } - Ok(()) -} - -async fn get_account_id(mut rpc_client: RpcClient) -> Result<()> { - let response = rpc_client.get_account_identity().await?; - println!("{response:#?}"); - Ok(()) -} - -async fn get_account_links(mut rpc_client: RpcClient, args: GetAccountLinksArgs) -> Result<()> { - let links = rpc_client.get_account_links(args.locale).await?; - println!("{links:?}"); - - Ok(()) -} - -async fn get_account_state(mut rpc_client: RpcClient) -> Result<()> { - let account_state = rpc_client.get_account_state().await?; - println!("{account_state}"); - Ok(()) -} - -async fn reset_device_identity( - mut rpc_client: RpcClient, - args: ResetDeviceIdentityArgs, -) -> Result<()> { - let seed = args.seed.map(|seed| seed.into_bytes()); - rpc_client.reset_device_identity(seed).await?; - Ok(()) -} - -async fn get_available_tickets(mut rpc_client: RpcClient) -> Result<()> { - let response = rpc_client.get_available_tickets().await?; - println!("{response:#?}"); - Ok(()) -} diff --git a/nym-vpn-core/crates/nym-vpnc/src/commands/mod.rs b/nym-vpn-core/crates/nym-vpnc/src/commands/mod.rs index ed099704cb..4ae6995b07 100644 --- a/nym-vpn-core/crates/nym-vpnc/src/commands/mod.rs +++ b/nym-vpn-core/crates/nym-vpnc/src/commands/mod.rs @@ -6,7 +6,6 @@ pub mod device; pub mod dns; pub mod gateway; pub mod lan; -pub mod legacy; pub mod network; pub mod network_stats; pub mod sentry; diff --git a/nym-vpn-core/crates/nym-vpnc/src/main.rs b/nym-vpn-core/crates/nym-vpnc/src/main.rs index e20fa6b90a..c727342982 100644 --- a/nym-vpn-core/crates/nym-vpnc/src/main.rs +++ b/nym-vpn-core/crates/nym-vpnc/src/main.rs @@ -15,6 +15,17 @@ use nym_vpn_proto::rpc_client::RpcClient; use crate::table_style::TableStyle; +#[tokio::main] +async fn main() -> Result<()> { + let args = ProgramArgs::parse(); + + let rpc_client = RpcClient::new() + .await + .context("Failed to create RPC client")?; + + args.command.execute(rpc_client).await +} + #[derive(Parser, Debug)] #[clap(version, about)] pub struct ProgramArgs { @@ -29,7 +40,8 @@ pub struct ProgramArgs { #[derive(clap::Subcommand, Debug)] pub enum Command { /// Connect the tunnel - ConnectV2 { + #[clap(alias = "connect-v2")] + Connect { /// Blocks until the connection is established or failed #[arg(short, long)] wait: bool, @@ -106,265 +118,256 @@ pub enum Command { #[command(subcommand)] subcommand: commands::network_stats::Command, }, - - #[command(flatten)] - Legacy(commands::legacy::Command), } -#[tokio::main] -async fn main() -> Result<()> { - let args = ProgramArgs::parse(); +impl Command { + pub async fn execute(self, rpc_client: RpcClient) -> Result<()> { + match self { + Command::Connect { wait } => Self::connect(rpc_client, wait).await, + Command::Reconnect => Self::reconnect(rpc_client).await, + Command::Disconnect { wait } => Self::disconnect(rpc_client, wait).await, + Command::Status { listen } => Self::status(rpc_client, listen).await, + Command::Info => Self::info(rpc_client).await, + Command::GetConfig => Self::get_config(rpc_client).await, + Command::Gateway(args) => args.execute(rpc_client).await, + Command::Tunnel { subcommand } => subcommand.execute(rpc_client).await, + Command::Lan { subcommand } => subcommand.execute(rpc_client).await, + Command::Dns { subcommand } => subcommand.execute(rpc_client).await, + Command::Network { subcommand } => subcommand.execute(rpc_client).await, + Command::Account { subcommand } => subcommand.execute(rpc_client).await, + Command::Device(args) => args.execute(rpc_client).await, + Command::Sentry { subcommand } => subcommand.execute(rpc_client).await, + Command::NetworkStats { subcommand } => subcommand.execute(rpc_client).await, + } + } - let rpc_client = RpcClient::new() - .await - .context("Failed to create RPC client")?; + async fn connect(mut rpc_client: RpcClient, wait: bool) -> Result<()> { + rpc_client.connect_tunnel().await?; - match args.command { - Command::ConnectV2 { wait } => connect_v2(rpc_client, wait).await, - Command::Reconnect => reconnect(rpc_client).await, - Command::Disconnect { wait } => disconnect(rpc_client, wait).await, - Command::Status { listen } => status(rpc_client, listen).await, - Command::Info => info(rpc_client).await, - Command::GetConfig => get_config(rpc_client).await, - Command::Gateway(args) => args.execute(rpc_client).await, - Command::Tunnel { subcommand } => subcommand.execute(rpc_client).await, - Command::Lan { subcommand } => subcommand.execute(rpc_client).await, - Command::Dns { subcommand } => subcommand.execute(rpc_client).await, - Command::Network { subcommand } => subcommand.execute(rpc_client).await, - Command::Account { subcommand } => subcommand.execute(rpc_client).await, - Command::Device(args) => args.execute(rpc_client).await, - Command::Sentry { subcommand } => subcommand.execute(rpc_client).await, - Command::NetworkStats { subcommand } => subcommand.execute(rpc_client).await, - Command::Legacy(subcommand) => subcommand.execute(rpc_client).await, + if wait { + println!("Waiting until connected or failed"); + Self::wait_until_connected(rpc_client).await + } else { + Ok(()) + } } -} - -async fn connect_v2(mut rpc_client: RpcClient, wait: bool) -> Result<()> { - rpc_client.connect_tunnel_v2().await?; - if wait { - println!("Waiting until connected or failed"); - wait_until_connected(rpc_client).await - } else { + async fn reconnect(mut rpc_client: RpcClient) -> Result<()> { + let _accepted = rpc_client.reconnect_tunnel().await?; Ok(()) } -} -async fn reconnect(mut rpc_client: RpcClient) -> Result<()> { - let _accepted = rpc_client.reconnect_tunnel().await?; - Ok(()) -} + async fn wait_until_connected(mut rpc_client: RpcClient) -> Result<()> { + let mut stream = rpc_client.listen_to_events().await?; + while let Some(new_state) = stream.next().await { + let TunnelEvent::NewState(new_state) = new_state? else { + continue; + }; + println!("{new_state}"); -async fn wait_until_connected(mut rpc_client: RpcClient) -> Result<()> { - let mut stream = rpc_client.listen_to_events().await?; - while let Some(new_state) = stream.next().await { - let TunnelEvent::NewState(new_state) = new_state? else { - continue; - }; - println!("{new_state}"); - - match new_state { - TunnelState::Connected { .. } => { - break; - } - TunnelState::Offline { reconnect } => { - if reconnect { - println!("Device is offline. Waiting for network connectivity."); - } else { - bail!("Device is offline"); + match new_state { + TunnelState::Connected { .. } => { + break; } + TunnelState::Offline { reconnect } => { + if reconnect { + println!("Device is offline. Waiting for network connectivity."); + } else { + bail!("Device is offline"); + } + } + TunnelState::Error(reason) => { + bail!("Tunnel entered error state {reason:?}"); + } + _ => {} } - TunnelState::Error(reason) => { - bail!("Tunnel entered error state {reason:?}"); - } - _ => {} } + Ok(()) } - Ok(()) -} - -async fn disconnect(mut rpc_client: RpcClient, wait: bool) -> Result<()> { - if wait { - let mut stream = rpc_client.clone().listen_to_events().await?; - println!("Waiting until disconnected"); + async fn disconnect(mut rpc_client: RpcClient, wait: bool) -> Result<()> { + if wait { + let mut stream = rpc_client.clone().listen_to_events().await?; - let current_state = rpc_client.get_tunnel_state().await?; - println!("{current_state}"); - - rpc_client.disconnect_tunnel().await?; - - if matches!( - current_state, - TunnelState::Disconnected | TunnelState::Offline { .. } - ) { - return Ok(()); - } + println!("Waiting until disconnected"); - while let Some(new_state) = stream.next().await { - let TunnelEvent::NewState(new_state) = new_state? else { - continue; - }; + let current_state = rpc_client.get_tunnel_state().await?; + println!("{current_state}"); - println!("{new_state}"); + rpc_client.disconnect_tunnel().await?; if matches!( - new_state, + current_state, TunnelState::Disconnected | TunnelState::Offline { .. } ) { return Ok(()); - } else if let TunnelState::Error(reason) = new_state { - bail!("Tunnel entered error state: {reason:?}") } + + while let Some(new_state) = stream.next().await { + let TunnelEvent::NewState(new_state) = new_state? else { + continue; + }; + + println!("{new_state}"); + + if matches!( + new_state, + TunnelState::Disconnected | TunnelState::Offline { .. } + ) { + return Ok(()); + } else if let TunnelState::Error(reason) = new_state { + bail!("Tunnel entered error state: {reason:?}") + } + } + Ok(()) + } else { + rpc_client.disconnect_tunnel().await?; + Ok(()) } - Ok(()) - } else { - rpc_client.disconnect_tunnel().await?; - Ok(()) } -} -async fn status(mut rpc_client: RpcClient, listen: bool) -> Result<()> { - let state = rpc_client.get_tunnel_state().await?; - println!("State: {state}"); + async fn status(mut rpc_client: RpcClient, listen: bool) -> Result<()> { + let state = rpc_client.get_tunnel_state().await?; + println!("State: {state}"); - if !listen { - return Ok(()); - } + if !listen { + return Ok(()); + } - let mut stream = rpc_client.listen_to_events().await?; + let mut stream = rpc_client.listen_to_events().await?; - while let Some(event) = stream.next().await { - match event { - Ok(TunnelEvent::NewState(new_state)) => { - println!("State: {new_state}"); - } - Ok(TunnelEvent::ConfigChanged(new_config)) => { - let json = serde_json::to_string_pretty(&new_config) - .context("failed to convert new config to JSON")?; - println!("Configuration changed:\n{json}"); + while let Some(event) = stream.next().await { + match event { + Ok(TunnelEvent::NewState(new_state)) => { + println!("State: {new_state}"); + } + Ok(TunnelEvent::ConfigChanged(new_config)) => { + let json = serde_json::to_string_pretty(&new_config) + .context("failed to convert new config to JSON")?; + println!("Configuration changed:\n{json}"); + } + _ => {} } - _ => {} } - } - Ok(()) -} + Ok(()) + } -async fn info(mut rpc_client: RpcClient) -> Result<()> { - let service_info = rpc_client.get_info().await?; - print_service_info(service_info); - Ok(()) -} + async fn info(mut rpc_client: RpcClient) -> Result<()> { + let service_info = rpc_client.get_info().await?; + Self::print_service_info(service_info); + Ok(()) + } -async fn get_config(mut rpc_client: RpcClient) -> Result<()> { - let config = rpc_client.get_config().await?; - println!("{config:#?}"); - Ok(()) -} + async fn get_config(mut rpc_client: RpcClient) -> Result<()> { + let config = rpc_client.get_config().await?; + println!("{config:#?}"); + Ok(()) + } -fn print_service_info(service_info: VpnServiceInfo) { - println!("nym-vpnd:"); - println!(" version: {}", service_info.version); - println!( - " build_timestamp (utc): {}", - service_info - .build_timestamp - .map(|s| s.to_string()) - .unwrap_or_default() - ); - println!(" triple: {}", service_info.triple); - println!(" platform: {}", service_info.platform); - println!(" git_commit: {}", service_info.git_commit); - println!(); - println!("nym_network:"); - println!(" network_name: {}", service_info.nym_network.network_name); - println!(" chain_details:"); - println!( - " bech32_account_prefix: {}", - service_info.nym_network.chain_details.bech32_account_prefix - ); - println!(" mix_denom:"); - println!( - " base: {}", - service_info.nym_network.chain_details.mix_denom.base - ); - println!( - " display: {}", - service_info.nym_network.chain_details.mix_denom.display - ); - println!( - " display_exponent: {}", - service_info - .nym_network - .chain_details - .mix_denom - .display_exponent - ); - println!(" stake_denom:"); - println!( - " base: {}", - service_info.nym_network.chain_details.stake_denom.base - ); - println!( - " display: {}", - service_info.nym_network.chain_details.stake_denom.display - ); - println!( - " display_exponent: {}", - service_info - .nym_network - .chain_details - .stake_denom - .display_exponent - ); - - println!(" validators:"); - for validator in &service_info.nym_network.endpoints { - println!(" nyxd_url: {}", validator.nyxd_url); - println!(" api_url: {}", or_not_set(&validator.api_url)); + fn print_service_info(service_info: VpnServiceInfo) { + println!("nym-vpnd:"); + println!(" version: {}", service_info.version); println!( - " websocket_url: {}", - or_not_set(&validator.websocket_url) + " build_timestamp (utc): {}", + service_info + .build_timestamp + .map(|s| s.to_string()) + .unwrap_or_default() ); - } - - println!(" nym_contracts:"); - println!( - " mixnet_contract_address: {}", - or_not_set(&service_info.nym_network.contracts.mixnet_contract_address) - ); - println!( - " vesting_contract_address: {}", - or_not_set(&service_info.nym_network.contracts.vesting_contract_address) - ); - println!( - " ecash_contract_address: {}", - or_not_set(&service_info.nym_network.contracts.ecash_contract_address) - ); - println!( - " group_contract_address: {}", - or_not_set(&service_info.nym_network.contracts.group_contract_address) - ); - println!( - " multisig_contract_address: {}", - or_not_set(&service_info.nym_network.contracts.multisig_contract_address) - ); - println!( - " coconut_dkg_contract_address: {}", - or_not_set( - &service_info + println!(" triple: {}", service_info.triple); + println!(" platform: {}", service_info.platform); + println!(" git_commit: {}", service_info.git_commit); + println!(); + println!("nym_network:"); + println!(" network_name: {}", service_info.nym_network.network_name); + println!(" chain_details:"); + println!( + " bech32_account_prefix: {}", + service_info.nym_network.chain_details.bech32_account_prefix + ); + println!(" mix_denom:"); + println!( + " base: {}", + service_info.nym_network.chain_details.mix_denom.base + ); + println!( + " display: {}", + service_info.nym_network.chain_details.mix_denom.display + ); + println!( + " display_exponent: {}", + service_info .nym_network - .contracts - .coconut_dkg_contract_address + .chain_details + .mix_denom + .display_exponent + ); + println!(" stake_denom:"); + println!( + " base: {}", + service_info.nym_network.chain_details.stake_denom.base + ); + println!( + " display: {}", + service_info.nym_network.chain_details.stake_denom.display + ); + println!( + " display_exponent: {}", + service_info + .nym_network + .chain_details + .stake_denom + .display_exponent + ); + + println!(" validators:"); + for validator in &service_info.nym_network.endpoints { + println!(" nyxd_url: {}", validator.nyxd_url); + println!(" api_url: {}", or_not_set(&validator.api_url)); + println!( + " websocket_url: {}", + or_not_set(&validator.websocket_url) + ); + } + + println!(" nym_contracts:"); + println!( + " mixnet_contract_address: {}", + or_not_set(&service_info.nym_network.contracts.mixnet_contract_address) + ); + println!( + " vesting_contract_address: {}", + or_not_set(&service_info.nym_network.contracts.vesting_contract_address) + ); + println!( + " ecash_contract_address: {}", + or_not_set(&service_info.nym_network.contracts.ecash_contract_address) + ); + println!( + " group_contract_address: {}", + or_not_set(&service_info.nym_network.contracts.group_contract_address) + ); + println!( + " multisig_contract_address: {}", + or_not_set(&service_info.nym_network.contracts.multisig_contract_address) + ); + println!( + " coconut_dkg_contract_address: {}", + or_not_set( + &service_info + .nym_network + .contracts + .coconut_dkg_contract_address + ) + ); + println!(); + println!("nym_vpn_network:"); + println!( + " nym_vpn_api_urls: {:?}", + service_info.nym_vpn_network.nym_vpn_api_urls ) - ); - println!(); - println!("nym_vpn_network:"); - println!( - " nym_vpn_api_urls: {:?}", - service_info.nym_vpn_network.nym_vpn_api_urls - ) + } } pub fn or_not_set(value: &Option) -> String { diff --git a/nym-vpn-core/crates/nym-vpnd/src/command_interface.rs b/nym-vpn-core/crates/nym-vpnd/src/command_interface.rs index ef9d0e810f..fb1936b105 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/command_interface.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/command_interface.rs @@ -16,8 +16,7 @@ use tokio_util::sync::CancellationToken; use tonic::transport::Server; use nym_vpn_lib_types::{ - ConnectArgs, EntryPoint, ExitPoint, GatewayFilters, ListGatewaysOptions, TargetState, - TunnelEvent, + EntryPoint, ExitPoint, GatewayFilters, ListGatewaysOptions, TargetState, TunnelEvent, }; use nym_vpn_proto::proto::{ @@ -197,6 +196,22 @@ impl NymVpnService for CommandInterface { Ok(tonic::Response::new(())) } + async fn set_enable_custom_dns( + &self, + request: tonic::Request, + ) -> Result> { + let enable_custom_dns = request.into_inner(); + + let _ = self + .send_and_wait(VpnServiceCommand::SetEnableCustomDns, enable_custom_dns) + .await + .map_err(|e| { + tonic::Status::internal(format!("Failed to set enable custom DNS: {e}")) + })?; + + Ok(tonic::Response::new(())) + } + async fn set_custom_dns( &self, request: tonic::Request, @@ -206,14 +221,8 @@ impl NymVpnService for CommandInterface { .try_into() .map_err(|e| tonic::Status::invalid_argument(format!("Invalid Custom DNS: {e}")))?; - let opt_custom_dns = if custom_dns.is_empty() { - None - } else { - Some(custom_dns) - }; - let _ = self - .send_and_wait(VpnServiceCommand::SetCustomDns, opt_custom_dns) + .send_and_wait(VpnServiceCommand::SetCustomDns, custom_dns) .await .map_err(|e| tonic::Status::internal(format!("Failed to set custom DNS: {e}")))?; @@ -292,23 +301,7 @@ impl NymVpnService for CommandInterface { Ok(tonic::Response::new(ipaddr_list)) } - async fn connect_tunnel( - &self, - request: tonic::Request, - ) -> Result> { - let connect_args = ConnectArgs::try_from(request.into_inner()) - .map_err(|err| tonic::Status::invalid_argument(err.to_string()))?; - - self.send_and_wait(VpnServiceCommand::Connect, connect_args) - .await?; - - Ok(tonic::Response::new(())) - } - - async fn connect_tunnel_v2( - &self, - _request: tonic::Request<()>, - ) -> Result> { + async fn connect_tunnel(&self, _request: tonic::Request<()>) -> Result> { let accepted = self .send_and_wait(VpnServiceCommand::SetTargetState, TargetState::Secured) .await?; diff --git a/nym-vpn-core/crates/nym-vpnd/src/config.rs b/nym-vpn-core/crates/nym-vpnd/src/config.rs index 31f496a308..6af01940a6 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/config.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/config.rs @@ -30,13 +30,10 @@ impl GlobalConfig { } pub async fn read_from_config_dir(config_dir: &Path) -> anyhow::Result { - let config = match Self::read_config(config_dir).await { - Ok(config) => config, - Err(err) => { - tracing::error!("Failed to read global config file; using default : {err}"); - GlobalConfig::default() - } - }; + let config = Self::read_config(config_dir).await.unwrap_or_else(|err| { + tracing::error!("Failed to read global config file; using default : {err}"); + GlobalConfig::default() + }); // Always write back config file back using the latest JSON version // TODO: Avoid doing this as it's double-writing the config file. diff --git a/nym-vpn-core/crates/nym-vpnd/src/service/config/config_manager.rs b/nym-vpn-core/crates/nym-vpnd/src/service/config/config_manager.rs index fcff528ad8..233697abeb 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/service/config/config_manager.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/service/config/config_manager.rs @@ -82,6 +82,7 @@ impl VpnServiceConfigManager { &self.config } + #[cfg(test)] pub async fn set_config(&mut self, config: nym_vpn_lib_types::VpnServiceConfig) { if self.config != config { self.config = config; @@ -145,7 +146,14 @@ impl VpnServiceConfigManager { } } - pub async fn set_custom_dns(&mut self, custom_dns: Option>) { + pub async fn set_enable_custom_dns(&mut self, enable_custom_dns: bool) { + if self.config.enable_custom_dns != enable_custom_dns { + self.config.enable_custom_dns = enable_custom_dns; + self.save_config_and_send_event().await; + } + } + + pub async fn set_custom_dns(&mut self, custom_dns: Vec) { if self.config.custom_dns != custom_dns { self.config.custom_dns = custom_dns; self.save_config_and_send_event().await; @@ -324,12 +332,11 @@ impl VpnServiceConfigManager { nym_vpn_lib_types::TunnelType::Mixnet }; - let dns = self - .config - .custom_dns - .as_ref() - .map(|addrs| DnsOptions::Custom(addrs.clone())) - .unwrap_or_default(); + let dns = if self.config.enable_custom_dns && !self.config.custom_dns.is_empty() { + DnsOptions::Custom(self.config.custom_dns.clone()) + } else { + DnsOptions::default() + }; TunnelSettings { enable_ipv6: !self.config.disable_ipv6, diff --git a/nym-vpn-core/crates/nym-vpnd/src/service/config/legacy.rs b/nym-vpn-core/crates/nym-vpnd/src/service/config/legacy.rs index 7b62b19e29..e683be3870 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/service/config/legacy.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/service/config/legacy.rs @@ -10,8 +10,8 @@ use serde::Deserialize; #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "snake_case")] pub(crate) struct VpnServiceConfig { - entry_point: EntryPoint, - exit_point: ExitPoint, + pub(crate) entry_point: EntryPoint, + pub(crate) exit_point: ExitPoint, } impl TryFrom for nym_vpn_lib_types::VpnServiceConfig { diff --git a/nym-vpn-core/crates/nym-vpnd/src/service/config/mod.rs b/nym-vpn-core/crates/nym-vpnd/src/service/config/mod.rs index 571cdbd288..6596c058c9 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/service/config/mod.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/service/config/mod.rs @@ -7,6 +7,7 @@ mod legacy; mod v1; mod v2; mod v3; +mod v4; #[cfg(test)] mod tests; @@ -23,6 +24,7 @@ use tokio::{ io::{self, AsyncWriteExt}, }; +use crate::service::config::entry_exit::v2::{EntryPoint, ExitPoint}; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; @@ -81,20 +83,19 @@ impl TryFrom<&str> for NetworkEnvironments { // External, versioned, representation of the vpn service config file. // -type VpnServiceConfigExtLatest = v3::VpnServiceConfig; - /// Represents the version of the vpn service config file. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] enum VpnServiceConfigVersion { V1, V2, V3, + V4, } impl VpnServiceConfigVersion { /// Returns the latest version of the config file. pub fn latest() -> Self { - VpnServiceConfigVersion::V3 + VpnServiceConfigVersion::V4 } } @@ -104,6 +105,7 @@ impl fmt::Display for VpnServiceConfigVersion { VpnServiceConfigVersion::V1 => "v1", VpnServiceConfigVersion::V2 => "v2", VpnServiceConfigVersion::V3 => "v3", + VpnServiceConfigVersion::V4 => "v4", }) } } @@ -115,6 +117,7 @@ enum VpnServiceConfigExt { V1(v1::VpnServiceConfig), V2(v2::VpnServiceConfig), V3(v3::VpnServiceConfig), + V4(v4::VpnServiceConfig), } impl VpnServiceConfigExt { @@ -123,6 +126,7 @@ impl VpnServiceConfigExt { VpnServiceConfigExt::V1(_) => VpnServiceConfigVersion::V1, VpnServiceConfigExt::V2(_) => VpnServiceConfigVersion::V2, VpnServiceConfigExt::V3(_) => VpnServiceConfigVersion::V3, + VpnServiceConfigExt::V4(_) => VpnServiceConfigVersion::V4, } } } @@ -135,6 +139,7 @@ impl TryFrom for nym_vpn_lib_types::VpnServiceConfig { VpnServiceConfigExt::V1(v1) => nym_vpn_lib_types::VpnServiceConfig::try_from(v1), VpnServiceConfigExt::V2(v2) => nym_vpn_lib_types::VpnServiceConfig::try_from(v2), VpnServiceConfigExt::V3(v3) => nym_vpn_lib_types::VpnServiceConfig::try_from(v3), + VpnServiceConfigExt::V4(v4) => nym_vpn_lib_types::VpnServiceConfig::try_from(v4), } } } @@ -143,8 +148,30 @@ impl TryFrom<&nym_vpn_lib_types::VpnServiceConfig> for VpnServiceConfigExt { type Error = ConfigSetupError; fn try_from(value: &nym_vpn_lib_types::VpnServiceConfig) -> Result { - let latest = VpnServiceConfigExtLatest::try_from(value)?; - Ok(latest.into()) + let custom_dns = value + .custom_dns + .iter() + .map(|ip| ip.to_string()) + .collect::>(); + + let v4 = v4::VpnServiceConfig { + entry_point: EntryPoint::try_from(&value.entry_point)?, + exit_point: ExitPoint::try_from(&value.exit_point)?, + allow_lan: value.allow_lan, + disable_ipv6: value.disable_ipv6, + enable_two_hop: value.enable_two_hop, + enable_bridges: value.enable_bridges, + netstack: value.netstack, + disable_poisson_rate: value.disable_poisson_rate, + disable_background_cover_traffic: value.disable_background_cover_traffic, + min_mixnode_performance: value.min_mixnode_performance, + min_gateway_mixnet_performance: value.min_gateway_mixnet_performance, + min_gateway_vpn_performance: value.min_gateway_vpn_performance, + residential_exit: value.residential_exit, + enable_custom_dns: value.enable_custom_dns, + custom_dns, + }; + Ok(VpnServiceConfigExt::V4(v4)) } } diff --git a/nym-vpn-core/crates/nym-vpnd/src/service/config/tests.rs b/nym-vpn-core/crates/nym-vpnd/src/service/config/tests.rs index c597222e41..7c17bae560 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/service/config/tests.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/service/config/tests.rs @@ -134,7 +134,7 @@ location = "BE" "#; let json_content = r#"{ - "version": "v3", + "version": "v4", "entry_point": { "country": { "two_letter_iso_country_code": "FR" @@ -156,7 +156,8 @@ location = "BE" "min_gateway_mixnet_performance": null, "min_gateway_vpn_performance": null, "residential_exit": false, - "custom_dns": null + "enable_custom_dns": false, + "custom_dns": [] }"#; let entry_point = nym_vpn_lib_types::EntryPoint::Country { @@ -181,7 +182,7 @@ identity = [ 99, 23, 98, 234, 66, 161, 195, 63, 155, 161, 250, 207, 17, 158, 136 "#; let json_content = r#"{ - "version": "v3", + "version": "v4", "entry_point": { "gateway": { "identity": "7CWjY3QFoA9dgE535u9bQiXCfzgMZvSpJu842GA1Wn42" @@ -203,7 +204,8 @@ identity = [ 99, 23, 98, 234, 66, 161, 195, 63, 155, 161, 250, 207, 17, 158, 136 "min_gateway_mixnet_performance": null, "min_gateway_vpn_performance": null, "residential_exit": false, - "custom_dns": null + "enable_custom_dns": false, + "custom_dns": [] }"#; let entry_point = nym_vpn_lib_types::EntryPoint::Gateway { @@ -283,7 +285,7 @@ exit_point = "Random" "#; let json_content = r#"{ - "version": "v3", + "version": "v4", "entry_point": "random", "exit_point": "random", "allow_lan": false, @@ -297,7 +299,8 @@ exit_point = "Random" "min_gateway_mixnet_performance": null, "min_gateway_vpn_performance": null, "residential_exit": false, - "custom_dns": null + "enable_custom_dns": false, + "custom_dns": [] }"#; let entry_point = nym_vpn_lib_types::EntryPoint::Random; @@ -323,7 +326,7 @@ async fn test_service_config_migrate_from_v1() { }"#; let json_latest_content = r#"{ - "version": "v3", + "version": "v4", "entry_point": { "gateway": { "identity": "7CWjY3QFoA9dgE535u9bQiXCfzgMZvSpJu842GA1Wn42" @@ -345,7 +348,8 @@ async fn test_service_config_migrate_from_v1() { "min_gateway_mixnet_performance": null, "min_gateway_vpn_performance": null, "residential_exit": false, - "custom_dns": null + "enable_custom_dns": false, + "custom_dns": [] }"#; run_migrate_json_test(json_v1_content, json_latest_content).await; @@ -380,7 +384,7 @@ async fn test_service_config_migrate_from_v2() { }"#; let json_latest_content = r#"{ - "version": "v3", + "version": "v4", "entry_point": { "gateway": { "identity": "7CWjY3QFoA9dgE535u9bQiXCfzgMZvSpJu842GA1Wn42" @@ -402,12 +406,77 @@ async fn test_service_config_migrate_from_v2() { "min_gateway_mixnet_performance": null, "min_gateway_vpn_performance": null, "residential_exit": false, - "custom_dns": ["192.168.50.1"] + "enable_custom_dns": true, + "custom_dns": [ "192.168.50.1" ] }"#; run_migrate_json_test(json_v2_content, json_latest_content).await; } +#[tokio::test] +async fn test_service_config_migrate_from_v3() { + let json_v3_content = r#"{ + "version": "v3", + "entry_point": { + "gateway": { + "identity": "7CWjY3QFoA9dgE535u9bQiXCfzgMZvSpJu842GA1Wn42" + } + }, + "exit_point": { + "address": { + "address": "MNrmKzuKjNdbEhfPUzVNfjw63oBQNSayqoQKGL4JjAV.6fDcSN6faGpvA3pd3riCwjpzXc7RQfWmGMa82UVoEwKE@d5adfJNtcdZW2XwK85JAAU8nXAs9JCPYn2RNvDLZn4e" + } + }, + "allow_lan": false, + "disable_ipv6": false, + "enable_two_hop": true, + "enable_bridges": false, + "netstack": false, + "disable_poisson_rate": false, + "disable_background_cover_traffic": false, + "min_mixnode_performance": null, + "min_gateway_mixnet_performance": null, + "min_gateway_vpn_performance": null, + "residential_exit": false, + "custom_dns": [ + "192.168.50.1", + "2001:db8:85a3::8a2e:370:7334" + ] +}"#; + + let json_latest_content = r#"{ + "version": "v4", + "entry_point": { + "gateway": { + "identity": "7CWjY3QFoA9dgE535u9bQiXCfzgMZvSpJu842GA1Wn42" + } + }, + "exit_point": { + "address": { + "address": "MNrmKzuKjNdbEhfPUzVNfjw63oBQNSayqoQKGL4JjAV.6fDcSN6faGpvA3pd3riCwjpzXc7RQfWmGMa82UVoEwKE@d5adfJNtcdZW2XwK85JAAU8nXAs9JCPYn2RNvDLZn4e" + } + }, + "allow_lan": false, + "disable_ipv6": false, + "enable_two_hop": true, + "enable_bridges": false, + "netstack": false, + "disable_poisson_rate": false, + "disable_background_cover_traffic": false, + "min_mixnode_performance": null, + "min_gateway_mixnet_performance": null, + "min_gateway_vpn_performance": null, + "residential_exit": false, + "enable_custom_dns": true, + "custom_dns": [ + "192.168.50.1", + "2001:db8:85a3::8a2e:370:7334" + ] +}"#; + + run_migrate_json_test(json_v3_content, json_latest_content).await; +} + #[tokio::test] async fn test_service_config_fallback_default_v1() { let broken_json_content = r#"{ @@ -429,7 +498,7 @@ async fn test_service_config_fallback_default_v1() { #[tokio::test] async fn test_service_config_fallback_default_v2() { let broken_json_content = r#"{ - "version": "v3", + "version": "v2", "entry_point": { "gateway": { "identity": "7CWjY3QFoA9dgE535u9bQiXCfzgMZvSpJu842GA1Wn42" @@ -456,6 +525,37 @@ async fn test_service_config_fallback_default_v2() { run_fallback_test(broken_json_content).await; } +#[tokio::test] +async fn test_service_config_fallback_default_v3() { + let broken_json_content = r#"{ + "version": "v3", + "entry_point": { + "gateway": { + "identity": "7CWjY3QFoA9dgE535u9bQiXCfzgMZvSpJu842GA1Wn42" + } + }, + "exit_point": { + "address": { + "address": "MNrmKzuKjNdbEhfPUzVNfjw63oBQNSayqoQKGL4JjAV.6fDcSN6faGpvA3pd3riCwjpzXc7RQfWmGMa82UVoEwKE@d5adfJNtcdZW2XwK85JAAU8nXAs9JCPYn2RNvDLZn4e" + } + }, + "dns": null, + "disable_ipv6": false, + "enable_two_hop": true, + "enable_bridges": false, + "netstack": false, + "disable_poisson_rate": false, + "disable_background_cover_traffic": false, + "min_mixnode_performance": null, + "min_gateway_mixnet_performance": null, + "min_gateway_vpn_performance": null, + "residential_exit": false, + "custom_dns": [ "1.2.3.4" ] +}"#; + + run_fallback_test(broken_json_content).await; +} + #[tokio::test] async fn test_service_config_serialize_defaults() { let config = nym_vpn_lib_types::VpnServiceConfig::default(); @@ -485,10 +585,11 @@ async fn test_service_config_serialize_full() { min_gateway_mixnet_performance: Some(64u8), min_gateway_vpn_performance: Some(1u8), residential_exit: true, - custom_dns: Some(vec![ + enable_custom_dns: true, + custom_dns: vec![ IpAddr::from_str("192.168.50.1").unwrap(), IpAddr::from_str("2001:db8:85a3::8a2e:370:7334").unwrap(), - ]), + ], }; run_serialize_test(config).await; } diff --git a/nym-vpn-core/crates/nym-vpnd/src/service/config/v1.rs b/nym-vpn-core/crates/nym-vpnd/src/service/config/v1.rs index 8362d21c3e..73d0687d48 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/service/config/v1.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/service/config/v1.rs @@ -12,8 +12,8 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct VpnServiceConfig { - entry_point: EntryPoint, - exit_point: ExitPoint, + pub entry_point: EntryPoint, + pub exit_point: ExitPoint, } impl From for VpnServiceConfigExt { diff --git a/nym-vpn-core/crates/nym-vpnd/src/service/config/v2.rs b/nym-vpn-core/crates/nym-vpnd/src/service/config/v2.rs index 5f0e03bb77..1bb2ca1a56 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/service/config/v2.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/service/config/v2.rs @@ -13,20 +13,20 @@ use std::{net::IpAddr, str::FromStr}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct VpnServiceConfig { - entry_point: EntryPoint, - exit_point: ExitPoint, - dns: Option, - allow_lan: bool, - disable_ipv6: bool, - enable_two_hop: bool, - enable_bridges: bool, - netstack: bool, - disable_poisson_rate: bool, - disable_background_cover_traffic: bool, - min_mixnode_performance: Option, - min_gateway_mixnet_performance: Option, - min_gateway_vpn_performance: Option, - residential_exit: bool, + pub entry_point: EntryPoint, + pub exit_point: ExitPoint, + pub dns: Option, + pub allow_lan: bool, + pub disable_ipv6: bool, + pub enable_two_hop: bool, + pub enable_bridges: bool, + pub netstack: bool, + pub disable_poisson_rate: bool, + pub disable_background_cover_traffic: bool, + pub min_mixnode_performance: Option, + pub min_gateway_mixnet_performance: Option, + pub min_gateway_vpn_performance: Option, + pub residential_exit: bool, } impl From for VpnServiceConfigExt { @@ -39,14 +39,14 @@ impl TryFrom for nym_vpn_lib_types::VpnServiceConfig { type Error = ConfigSetupError; fn try_from(value: VpnServiceConfig) -> Result { - let custom_dns = value - .dns - .map(|addr| { - IpAddr::from_str(&addr) - .map(|ip| vec![ip]) - .map_err(|e| ConfigSetupError::IpAddress { error: Box::new(e) }) - }) - .transpose()?; + let custom_dns = match value.dns { + Some(str) => { + let ip = IpAddr::from_str(&str) + .map_err(|e| ConfigSetupError::IpAddress { error: Box::new(e) })?; + vec![ip] + } + None => vec![], + }; let config = nym_vpn_lib_types::VpnServiceConfig { entry_point: nym_vpn_lib_types::EntryPoint::try_from(value.entry_point)?, @@ -62,6 +62,7 @@ impl TryFrom for nym_vpn_lib_types::VpnServiceConfig { min_gateway_mixnet_performance: value.min_gateway_mixnet_performance, min_gateway_vpn_performance: value.min_gateway_vpn_performance, residential_exit: value.residential_exit, + enable_custom_dns: !custom_dns.is_empty(), custom_dns, }; Ok(config) diff --git a/nym-vpn-core/crates/nym-vpnd/src/service/config/v3.rs b/nym-vpn-core/crates/nym-vpnd/src/service/config/v3.rs index e4c1ab7253..fe11d7c9c8 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/service/config/v3.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/service/config/v3.rs @@ -4,7 +4,7 @@ use crate::service::{ ConfigSetupError, config::{ - VpnServiceConfigExt, VpnServiceConfigExtLatest, + VpnServiceConfigExt, entry_exit::v2::{EntryPoint, ExitPoint}, }, }; @@ -13,20 +13,20 @@ use std::{net::IpAddr, str::FromStr}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct VpnServiceConfig { - entry_point: EntryPoint, - exit_point: ExitPoint, - allow_lan: bool, - disable_ipv6: bool, - enable_two_hop: bool, - enable_bridges: bool, - netstack: bool, - disable_poisson_rate: bool, - disable_background_cover_traffic: bool, - min_mixnode_performance: Option, - min_gateway_mixnet_performance: Option, - min_gateway_vpn_performance: Option, - residential_exit: bool, - custom_dns: Option>, + pub entry_point: EntryPoint, + pub exit_point: ExitPoint, + pub allow_lan: bool, + pub disable_ipv6: bool, + pub enable_two_hop: bool, + pub enable_bridges: bool, + pub netstack: bool, + pub disable_poisson_rate: bool, + pub disable_background_cover_traffic: bool, + pub min_mixnode_performance: Option, + pub min_gateway_mixnet_performance: Option, + pub min_gateway_vpn_performance: Option, + pub residential_exit: bool, + pub custom_dns: Option>, } impl From for VpnServiceConfigExt { @@ -39,18 +39,17 @@ impl TryFrom for nym_vpn_lib_types::VpnServiceConfig { type Error = ConfigSetupError; fn try_from(value: VpnServiceConfig) -> Result { - let custom_dns: Option> = value - .custom_dns - .map(|dns_list| { - dns_list - .into_iter() - .map(|addr| { - IpAddr::from_str(&addr) - .map_err(|e| ConfigSetupError::IpAddress { error: Box::new(e) }) - }) - .collect::, ConfigSetupError>>() - }) - .transpose()?; + let custom_dns = match &value.custom_dns { + None => vec![], + Some(dns_list) if dns_list.is_empty() => vec![], + Some(dns_list) => dns_list + .iter() + .map(|dns_str| { + IpAddr::from_str(dns_str) + .map_err(|e| ConfigSetupError::IpAddress { error: Box::new(e) }) + }) + .collect::>()?, + }; let config = nym_vpn_lib_types::VpnServiceConfig { entry_point: nym_vpn_lib_types::EntryPoint::try_from(value.entry_point)?, @@ -66,41 +65,9 @@ impl TryFrom for nym_vpn_lib_types::VpnServiceConfig { min_gateway_mixnet_performance: value.min_gateway_mixnet_performance, min_gateway_vpn_performance: value.min_gateway_vpn_performance, residential_exit: value.residential_exit, + enable_custom_dns: !custom_dns.is_empty(), custom_dns, }; Ok(config) } } - -// This is only required for the latest configuration version. -impl TryFrom<&nym_vpn_lib_types::VpnServiceConfig> for VpnServiceConfigExtLatest { - type Error = ConfigSetupError; - - fn try_from(value: &nym_vpn_lib_types::VpnServiceConfig) -> Result { - let custom_dns = match &value.custom_dns { - None => None, - Some(dns_list) => { - let string_list: Vec = - dns_list.iter().map(|addr| addr.to_string()).collect(); - Some(string_list) - } - }; - let ext_config = VpnServiceConfigExtLatest { - entry_point: EntryPoint::try_from(&value.entry_point)?, - exit_point: ExitPoint::try_from(&value.exit_point)?, - allow_lan: value.allow_lan, - disable_ipv6: value.disable_ipv6, - enable_two_hop: value.enable_two_hop, - enable_bridges: value.enable_bridges, - netstack: value.netstack, - disable_poisson_rate: value.disable_poisson_rate, - disable_background_cover_traffic: value.disable_background_cover_traffic, - min_mixnode_performance: value.min_mixnode_performance, - min_gateway_mixnet_performance: value.min_gateway_mixnet_performance, - min_gateway_vpn_performance: value.min_gateway_vpn_performance, - residential_exit: value.residential_exit, - custom_dns, - }; - Ok(ext_config) - } -} diff --git a/nym-vpn-core/crates/nym-vpnd/src/service/config/v4.rs b/nym-vpn-core/crates/nym-vpnd/src/service/config/v4.rs new file mode 100644 index 0000000000..4efa4df95e --- /dev/null +++ b/nym-vpn-core/crates/nym-vpnd/src/service/config/v4.rs @@ -0,0 +1,71 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::service::{ + ConfigSetupError, + config::{ + VpnServiceConfigExt, + entry_exit::v2::{EntryPoint, ExitPoint}, + }, +}; +use serde::{Deserialize, Serialize}; +use std::{net::IpAddr, str::FromStr}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct VpnServiceConfig { + pub entry_point: EntryPoint, + pub exit_point: ExitPoint, + pub allow_lan: bool, + pub disable_ipv6: bool, + pub enable_two_hop: bool, + pub enable_bridges: bool, + pub netstack: bool, + pub disable_poisson_rate: bool, + pub disable_background_cover_traffic: bool, + pub min_mixnode_performance: Option, + pub min_gateway_mixnet_performance: Option, + pub min_gateway_vpn_performance: Option, + pub residential_exit: bool, + pub enable_custom_dns: bool, + pub custom_dns: Vec, +} + +impl From for VpnServiceConfigExt { + fn from(v4: VpnServiceConfig) -> Self { + VpnServiceConfigExt::V4(v4) + } +} + +impl TryFrom for nym_vpn_lib_types::VpnServiceConfig { + type Error = ConfigSetupError; + + fn try_from(value: VpnServiceConfig) -> Result { + let custom_dns: Vec = value + .custom_dns + .iter() + .map(|dns_str| { + IpAddr::from_str(dns_str) + .map_err(|e| ConfigSetupError::IpAddress { error: Box::new(e) }) + }) + .collect::>()?; + + let config = nym_vpn_lib_types::VpnServiceConfig { + entry_point: nym_vpn_lib_types::EntryPoint::try_from(value.entry_point)?, + exit_point: nym_vpn_lib_types::ExitPoint::try_from(value.exit_point)?, + allow_lan: value.allow_lan, + disable_ipv6: value.disable_ipv6, + enable_two_hop: value.enable_two_hop, + enable_bridges: value.enable_bridges, + netstack: value.netstack, + disable_poisson_rate: value.disable_poisson_rate, + disable_background_cover_traffic: value.disable_background_cover_traffic, + min_mixnode_performance: value.min_mixnode_performance, + min_gateway_mixnet_performance: value.min_gateway_mixnet_performance, + min_gateway_vpn_performance: value.min_gateway_vpn_performance, + residential_exit: value.residential_exit, + enable_custom_dns: value.enable_custom_dns, + custom_dns, + }; + Ok(config) + } +} diff --git a/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs b/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs index f348459fea..767aa29140 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs @@ -39,7 +39,7 @@ use nym_vpn_lib::{ tunnel_state_machine::{NymConfig, TunnelCommand, TunnelConstants, TunnelStateMachine}, }; use nym_vpn_lib_types::{ - AccountBalanceResponse, AccountCommandError, AccountControllerState, ConnectArgs, + AccountBalanceResponse, AccountCommandError, AccountControllerState, DecentralisedObtainTicketbooksRequest, EntryPoint, ExitPoint, FeatureFlags, Gateway, GatewayFilters, ListGatewaysOptions, LogPath, NetworkCompatibility, NymNetworkDetails, NymVpnDevice, NymVpnNetwork, NymVpnUsage, ParsedAccountLinks, StoreAccountRequest, @@ -66,7 +66,8 @@ pub enum VpnServiceCommand { SetAllowLan(oneshot::Sender<()>, bool), SetEnableBridges(oneshot::Sender<()>, bool), SetResidentialExit(oneshot::Sender<()>, bool), - SetCustomDns(oneshot::Sender<()>, Option>), + SetEnableCustomDns(oneshot::Sender<()>, bool), + SetCustomDns(oneshot::Sender<()>, Vec), SetNetwork(oneshot::Sender>, String), GetSystemMessages(oneshot::Sender>, ()), GetNetworkCompatibility(oneshot::Sender>, ()), @@ -86,8 +87,6 @@ pub enum VpnServiceCommand { ), DisableSocks5(oneshot::Sender>, ()), GetSocks5Status(oneshot::Sender>, ()), - // Deprecated - Connect(oneshot::Sender<()>, ConnectArgs), SetTargetState(oneshot::Sender, TargetState), Reconnect(oneshot::Sender, ()), GetTunnelState(oneshot::Sender, ()), @@ -774,6 +773,10 @@ impl NymVpnService { self.handle_set_residential_exit(residential_exit).await; let _ = tx.send(()); } + VpnServiceCommand::SetEnableCustomDns(tx, enable_custom_dns) => { + self.handle_set_enable_custom_dns(enable_custom_dns).await; + let _ = tx.send(()); + } VpnServiceCommand::SetCustomDns(tx, custom_dns) => { self.handle_set_custom_dns(custom_dns).await; let _ = tx.send(()); @@ -804,10 +807,6 @@ impl NymVpnService { VpnServiceCommand::ListFilteredGateways(tx, filters) => { self.handle_list_filtered_gateways(filters, tx).await; } - VpnServiceCommand::Connect(tx, connect_args) => { - self.handle_connect(connect_args).await.ok(); - let _ = tx.send(()); - } VpnServiceCommand::SetTargetState(tx, target_state) => { let accepted = self.set_target_state(target_state).await; let _ = tx.send(accepted); @@ -972,7 +971,14 @@ impl NymVpnService { self.update_tunnel_settings_with_throttle(); } - async fn handle_set_custom_dns(&mut self, custom_dns: Option>) { + async fn handle_set_enable_custom_dns(&mut self, enable_custom_dns: bool) { + self.config_manager + .set_enable_custom_dns(enable_custom_dns) + .await; + self.update_tunnel_settings_with_throttle(); + } + + async fn handle_set_custom_dns(&mut self, custom_dns: Vec) { self.config_manager.set_custom_dns(custom_dns).await; self.update_tunnel_settings_with_throttle(); } @@ -1231,53 +1237,6 @@ impl NymVpnService { self.socks5_service.get_status().await } - // Deprecated - async fn handle_connect(&mut self, connect_args: ConnectArgs) -> Result<()> { - let ConnectArgs { - entry, - exit, - options, - } = connect_args; - - let entry_point = entry.unwrap_or(self.config_manager.config().entry_point.clone()); - let exit_point = exit.unwrap_or(self.config_manager.config().exit_point.clone()); - let custom_dns = options.dns.as_ref().map(|ip_addr| vec![*ip_addr]); - let config = VpnServiceConfig { - entry_point, - exit_point, - disable_ipv6: options.disable_ipv6, - enable_two_hop: options.enable_two_hop, - enable_bridges: options.enable_bridges, - netstack: options.netstack, - allow_lan: true, // always true to support legacy behavior - min_mixnode_performance: None, - min_gateway_mixnet_performance: None, - min_gateway_vpn_performance: None, - disable_poisson_rate: options.disable_poisson_rate, - disable_background_cover_traffic: options.disable_background_cover_traffic, - residential_exit: false, - custom_dns, - }; - - self.config_manager.set_config(config).await; - - self.statistics_event_sender - .report(StatisticsEvent::new_connecting( - self.config_manager.config().enable_two_hop, - )); - - self.update_tunnel_settings(); - - // Ensure to always reconnect to maintain the legacy behavior - if self.target_state == TargetState::Secured { - let _ = self.command_sender.send(TunnelCommand::Connect); - } else { - let _ = self.set_target_state(TargetState::Secured).await; - } - - Ok(()) - } - async fn handle_get_tunnel_state(&self) -> TunnelState { self.tunnel_state.read().await.clone() }