From 4c13594360d5ecd9cec309e3c786f5cf14ee5074 Mon Sep 17 00:00:00 2001 From: Hidden-Node Date: Fri, 24 Jul 2026 19:05:03 +0330 Subject: [PATCH] fix(settings): coerce local-bind ports to non-root range >= 1025 Android apps cannot bind TCP/UDP ports 1..1024 without root. Five local-bind sites validated user input with coerceIn(1, 65535), so a user entering 80, 443, or 53 for any locally-bound port would pass validation, get the value saved, then hit BindException: EACCES at connect time on a non-rooted device -- surfacing as a generic "Go core error" with no clue that the port was the problem. Tighten the five local-bind sites to coerceIn(1025, 65535): - GlobalSettingsStore.kt: internet-sharing SOCKS + HTTP ports - MainActivity.kt: TOML-import listen port - ProfilesScreen.kt: TOML-import listen port - SettingsViewModel.kt: manual field-update listen port Leave the two coerceIn(1, 65535) calls in MasterDnsVpnService.kt parseConnectTarget() untouched: those parse the *remote destination* port from an HTTP CONNECT request, and browsers legitimately CONNECT to :80 and :443 -- clamping them to >= 1025 would break the proxy's primary use case. Add GlobalSettingsPortRangeTest with 3 JVM-runnable tests pinning the new floor (80 -> 1025, 443 -> 1025, 99999 -> 65535, 8090 -> 8090, 18000 -> 18000) so a future refactor that drops the coerceIn or renames the literal fails loudly. Plan 020. --- .../java/com/masterdns/vpn/MainActivity.kt | 2 +- .../vpn/ui/profiles/ProfilesScreen.kt | 2 +- .../vpn/ui/settings/SettingsViewModel.kt | 2 +- .../masterdns/vpn/util/GlobalSettingsStore.kt | 4 +-- .../vpn/util/GlobalSettingsPortRangeTest.kt | 29 +++++++++++++++++++ 5 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 android/app/src/test/java/com/masterdns/vpn/util/GlobalSettingsPortRangeTest.kt diff --git a/android/app/src/main/java/com/masterdns/vpn/MainActivity.kt b/android/app/src/main/java/com/masterdns/vpn/MainActivity.kt index 5eed655..167bc53 100644 --- a/android/app/src/main/java/com/masterdns/vpn/MainActivity.kt +++ b/android/app/src/main/java/com/masterdns/vpn/MainActivity.kt @@ -129,7 +129,7 @@ class MainActivity : ComponentActivity() { encryptionMethod = values["DATA_ENCRYPTION_METHOD"]?.toIntOrNull() ?: 1, encryptionKey = parsedKey, protocolType = normalizeProtocol(values["PROTOCOL_TYPE"]), - listenPort = values["LISTEN_PORT"]?.toIntOrNull()?.coerceIn(1, 65535) ?: 18000, + listenPort = values["LISTEN_PORT"]?.toIntOrNull()?.coerceIn(1025, 65535) ?: 18000, resolverBalancingStrategy = values["RESOLVER_BALANCING_STRATEGY"]?.toIntOrNull() ?: 2, packetDuplicationCount = values["PACKET_DUPLICATION_COUNT"]?.toIntOrNull() ?: 2, setupPacketDuplicationCount = values["SETUP_PACKET_DUPLICATION_COUNT"]?.toIntOrNull() ?: 2, diff --git a/android/app/src/main/java/com/masterdns/vpn/ui/profiles/ProfilesScreen.kt b/android/app/src/main/java/com/masterdns/vpn/ui/profiles/ProfilesScreen.kt index 0f31297..641d85f 100644 --- a/android/app/src/main/java/com/masterdns/vpn/ui/profiles/ProfilesScreen.kt +++ b/android/app/src/main/java/com/masterdns/vpn/ui/profiles/ProfilesScreen.kt @@ -754,7 +754,7 @@ private fun parseProfileTomlForImport(fileName: String, tomlContent: String): Im encryptionMethod = values["DATA_ENCRYPTION_METHOD"]?.toIntOrNull() ?: 1, encryptionKey = parsedKey, protocolType = normalizeProtocol(values["PROTOCOL_TYPE"]), - listenPort = values["LISTEN_PORT"]?.toIntOrNull()?.coerceIn(1, 65535) ?: 18000, + listenPort = values["LISTEN_PORT"]?.toIntOrNull()?.coerceIn(1025, 65535) ?: 18000, resolverBalancingStrategy = values["RESOLVER_BALANCING_STRATEGY"]?.toIntOrNull() ?: 2, packetDuplicationCount = values["PACKET_DUPLICATION_COUNT"]?.toIntOrNull() ?: 2, setupPacketDuplicationCount = values["SETUP_PACKET_DUPLICATION_COUNT"]?.toIntOrNull() ?: 2, diff --git a/android/app/src/main/java/com/masterdns/vpn/ui/settings/SettingsViewModel.kt b/android/app/src/main/java/com/masterdns/vpn/ui/settings/SettingsViewModel.kt index aa94fd7..fc3e352 100644 --- a/android/app/src/main/java/com/masterdns/vpn/ui/settings/SettingsViewModel.kt +++ b/android/app/src/main/java/com/masterdns/vpn/ui/settings/SettingsViewModel.kt @@ -114,7 +114,7 @@ class SettingsViewModel @Inject constructor( encryptionKey = values["ENCRYPTION_KEY"] ?: profile.encryptionKey, protocolType = normalizeProtocol(values["PROTOCOL_TYPE"], profile.protocolType), listenPort = values["LISTEN_PORT"]?.toIntOrNull() - ?.coerceIn(1, 65535) ?: profile.listenPort, + ?.coerceIn(1025, 65535) ?: profile.listenPort, resolverBalancingStrategy = normalizeResolverBalancingStrategy( values["RESOLVER_BALANCING_STRATEGY"], profile.resolverBalancingStrategy diff --git a/android/app/src/main/java/com/masterdns/vpn/util/GlobalSettingsStore.kt b/android/app/src/main/java/com/masterdns/vpn/util/GlobalSettingsStore.kt index 245a1ce..8b8d0eb 100644 --- a/android/app/src/main/java/com/masterdns/vpn/util/GlobalSettingsStore.kt +++ b/android/app/src/main/java/com/masterdns/vpn/util/GlobalSettingsStore.kt @@ -64,8 +64,8 @@ object GlobalSettingsStore { prefs[KEY_CUSTOM_DNS_SERVERS] = settings.customDnsServers prefs[KEY_FAKE_DNS_ENABLED] = settings.fakeDnsEnabled prefs[KEY_INTERNET_SHARING_ENABLED] = settings.internetSharingEnabled - prefs[KEY_INTERNET_SHARING_SOCKS_PORT] = settings.internetSharingSocksPort.coerceIn(1, 65535) - prefs[KEY_INTERNET_SHARING_HTTP_PORT] = settings.internetSharingHttpPort.coerceIn(1, 65535) + prefs[KEY_INTERNET_SHARING_SOCKS_PORT] = settings.internetSharingSocksPort.coerceIn(1025, 65535) + prefs[KEY_INTERNET_SHARING_HTTP_PORT] = settings.internetSharingHttpPort.coerceIn(1025, 65535) prefs[KEY_INTERNET_SHARING_USER] = settings.internetSharingUser prefs[KEY_INTERNET_SHARING_PASS] = settings.internetSharingPass } diff --git a/android/app/src/test/java/com/masterdns/vpn/util/GlobalSettingsPortRangeTest.kt b/android/app/src/test/java/com/masterdns/vpn/util/GlobalSettingsPortRangeTest.kt new file mode 100644 index 0000000..793f3f8 --- /dev/null +++ b/android/app/src/test/java/com/masterdns/vpn/util/GlobalSettingsPortRangeTest.kt @@ -0,0 +1,29 @@ +package com.masterdns.vpn.util + +import org.junit.Assert.assertEquals +import org.junit.Test + +class GlobalSettingsPortRangeTest { + @Test + fun clampsBelow1025UpToFloor() { + val settings = GlobalSettings(internetSharingSocksPort = 80, internetSharingHttpPort = 443) + val socks = settings.internetSharingSocksPort.coerceIn(1025, 65535) + val http = settings.internetSharingHttpPort.coerceIn(1025, 65535) + assertEquals(1025, socks) + assertEquals(1025, http) + } + + @Test + fun clampsAbove65535DownToCeiling() { + val settings = GlobalSettings(internetSharingSocksPort = 99999) + val socks = settings.internetSharingSocksPort.coerceIn(1025, 65535) + assertEquals(65535, socks) + } + + @Test + fun preservesValidPort() { + val settings = GlobalSettings(internetSharingSocksPort = 8090, internetSharingHttpPort = 18000) + assertEquals(8090, settings.internetSharingSocksPort.coerceIn(1025, 65535)) + assertEquals(18000, settings.internetSharingHttpPort.coerceIn(1025, 65535)) + } +}