Skip to content

Commit d3b0a06

Browse files
authored
fix(vpn): IPv6 CONNECT parse, native Int port storage, malformed-intent guard (#19) (#45)
* fix(proxy): parse IPv6-literal CONNECT targets correctly * fix(settings): store sharing ports as native Int with range validation * fix(service): log and tear down on malformed ACTION_CONNECT intent * test(service): add unit tests for parseConnectTarget
1 parent 3ffb861 commit d3b0a06

3 files changed

Lines changed: 81 additions & 10 deletions

File tree

android/app/src/main/java/com/masterdns/vpn/service/MasterDnsVpnService.kt

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,25 @@ class MasterDnsVpnService : VpnService() {
6060
"com.android.chrome" // system Chrome on some OEMs
6161
)
6262

63-
63+
internal fun parseConnectTarget(url: String): Pair<String, Int> {
64+
if (url.startsWith("[")) {
65+
val close = url.indexOf("]")
66+
if (close > 0) {
67+
val host = url.substring(1, close)
68+
val portPart = url.substring(close + 1).removePrefix(":")
69+
val port = portPart.toIntOrNull()?.coerceIn(1, 65535) ?: 80
70+
return host to port
71+
}
72+
}
73+
val lastColon = url.lastIndexOf(':')
74+
return if (lastColon > 0) {
75+
val host = url.substring(0, lastColon)
76+
val port = url.substring(lastColon + 1).toIntOrNull()?.coerceIn(1, 65535) ?: 80
77+
host to port
78+
} else {
79+
url to 80
80+
}
81+
}
6482
}
6583

6684
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@@ -122,6 +140,19 @@ class MasterDnsVpnService : VpnService() {
122140
val profileId = intent.getLongExtra(EXTRA_PROFILE_ID, -1)
123141
if (profileId > 0) {
124142
startVpn(profileId)
143+
} else {
144+
val msg = "Invalid profile id: $profileId"
145+
VpnManager.appendLog(msg)
146+
VpnManager.setError(msg)
147+
runCatching {
148+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
149+
stopForeground(STOP_FOREGROUND_REMOVE)
150+
} else {
151+
@Suppress("DEPRECATION")
152+
stopForeground(true)
153+
}
154+
}
155+
runCatching { stopSelf() }
125156
}
126157
}
127158
ACTION_DISCONNECT -> {
@@ -979,9 +1010,7 @@ class MasterDnsVpnService : VpnService() {
9791010
}
9801011

9811012
if (method == "CONNECT") {
982-
val hostPort = url.split(":")
983-
val host = hostPort[0]
984-
val port = hostPort.getOrElse(1) { "80" }.toIntOrNull() ?: 80
1013+
val (host, port) = parseConnectTarget(url)
9851014

9861015
output.write("HTTP/1.1 200 Connection Established\r\n\r\n")
9871016
output.flush()

android/app/src/main/java/com/masterdns/vpn/util/GlobalSettingsStore.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.Context
44
import androidx.datastore.preferences.core.Preferences
55
import androidx.datastore.preferences.core.booleanPreferencesKey
66
import androidx.datastore.preferences.core.edit
7+
import androidx.datastore.preferences.core.intPreferencesKey
78
import androidx.datastore.preferences.core.stringPreferencesKey
89
import androidx.datastore.preferences.preferencesDataStore
910
import kotlinx.coroutines.flow.Flow
@@ -38,8 +39,8 @@ object GlobalSettingsStore {
3839
private val KEY_CUSTOM_DNS_SERVERS = stringPreferencesKey("custom_dns_servers")
3940
private val KEY_FAKE_DNS_ENABLED = booleanPreferencesKey("fake_dns_enabled")
4041
private val KEY_INTERNET_SHARING_ENABLED = booleanPreferencesKey("internet_sharing_enabled")
41-
private val KEY_INTERNET_SHARING_SOCKS_PORT = stringPreferencesKey("internet_sharing_socks_port")
42-
private val KEY_INTERNET_SHARING_HTTP_PORT = stringPreferencesKey("internet_sharing_http_port")
42+
private val KEY_INTERNET_SHARING_SOCKS_PORT = intPreferencesKey("internet_sharing_socks_port_v2")
43+
private val KEY_INTERNET_SHARING_HTTP_PORT = intPreferencesKey("internet_sharing_http_port_v2")
4344
private val KEY_INTERNET_SHARING_USER = stringPreferencesKey("internet_sharing_user")
4445
private val KEY_INTERNET_SHARING_PASS = stringPreferencesKey("internet_sharing_pass")
4546

@@ -63,8 +64,8 @@ object GlobalSettingsStore {
6364
prefs[KEY_CUSTOM_DNS_SERVERS] = settings.customDnsServers
6465
prefs[KEY_FAKE_DNS_ENABLED] = settings.fakeDnsEnabled
6566
prefs[KEY_INTERNET_SHARING_ENABLED] = settings.internetSharingEnabled
66-
prefs[KEY_INTERNET_SHARING_SOCKS_PORT] = settings.internetSharingSocksPort.toString()
67-
prefs[KEY_INTERNET_SHARING_HTTP_PORT] = settings.internetSharingHttpPort.toString()
67+
prefs[KEY_INTERNET_SHARING_SOCKS_PORT] = settings.internetSharingSocksPort.coerceIn(1, 65535)
68+
prefs[KEY_INTERNET_SHARING_HTTP_PORT] = settings.internetSharingHttpPort.coerceIn(1, 65535)
6869
prefs[KEY_INTERNET_SHARING_USER] = settings.internetSharingUser
6970
prefs[KEY_INTERNET_SHARING_PASS] = settings.internetSharingPass
7071
}
@@ -80,8 +81,8 @@ object GlobalSettingsStore {
8081
customDnsServers = this[KEY_CUSTOM_DNS_SERVERS] ?: "",
8182
fakeDnsEnabled = this[KEY_FAKE_DNS_ENABLED] ?: true,
8283
internetSharingEnabled = this[KEY_INTERNET_SHARING_ENABLED] ?: false,
83-
internetSharingSocksPort = this[KEY_INTERNET_SHARING_SOCKS_PORT]?.toIntOrNull() ?: 8090,
84-
internetSharingHttpPort = this[KEY_INTERNET_SHARING_HTTP_PORT]?.toIntOrNull() ?: 8091,
84+
internetSharingSocksPort = this[KEY_INTERNET_SHARING_SOCKS_PORT] ?: 8090,
85+
internetSharingHttpPort = this[KEY_INTERNET_SHARING_HTTP_PORT] ?: 8091,
8586
internetSharingUser = this[KEY_INTERNET_SHARING_USER] ?: "",
8687
internetSharingPass = this[KEY_INTERNET_SHARING_PASS] ?: ""
8788
)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.masterdns.vpn.service
2+
3+
import org.junit.Assert.assertEquals
4+
import org.junit.Test
5+
6+
class MasterDnsVpnServiceConnectParseTest {
7+
@Test
8+
fun parsesIpv6LiteralWithPort() {
9+
val (host, port) = MasterDnsVpnService.parseConnectTarget("[::1]:443")
10+
assertEquals("::1", host)
11+
assertEquals(443, port)
12+
}
13+
14+
@Test
15+
fun parsesIpv6LiteralWithoutPort() {
16+
val (host, port) = MasterDnsVpnService.parseConnectTarget("[::1]")
17+
assertEquals("::1", host)
18+
assertEquals(80, port)
19+
}
20+
21+
@Test
22+
fun parsesIpv4WithPort() {
23+
val (host, port) = MasterDnsVpnService.parseConnectTarget("127.0.0.1:8080")
24+
assertEquals("127.0.0.1", host)
25+
assertEquals(8080, port)
26+
}
27+
28+
@Test
29+
fun parsesDomainWithoutPort() {
30+
val (host, port) = MasterDnsVpnService.parseConnectTarget("example.com")
31+
assertEquals("example.com", host)
32+
assertEquals(80, port)
33+
}
34+
35+
@Test
36+
fun coercesOutOfRangePortToClampedValue() {
37+
val (host, port) = MasterDnsVpnService.parseConnectTarget("example.com:99999")
38+
assertEquals("example.com", host)
39+
assertEquals(65535, port)
40+
}
41+
}

0 commit comments

Comments
 (0)