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.
+
+
+
+ Apply and reconnect now
+
+ {
+ console.log('cancel');
+ onClose();
+ }}
+ className="min-w-32"
+ color="gray"
+ outline
+ >
+ Discard changes
+
+
+
+ );
+}
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}
+ )}
+
+
+
+
+ {t('dns.details.add')}
+
+
+
+
+ )}
+
+
+
+ {t('dns.details.apply')}
+
+
+ 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) => (
+
+ ))}
+
+
+ );
+}
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()
}