From 54f01d67ccc73db72c3c50bfc6e44b403b62e936 Mon Sep 17 00:00:00 2001 From: CallMeTechie <34693633+CallMeTechie@users.noreply.github.com> Date: Sun, 24 May 2026 13:29:38 +0200 Subject: [PATCH 1/7] feat(rdp): use server connect_address/connect_port as RDP connection target --- .../gatecontrol/android/network/ApiModels.kt | 2 ++ .../android/rdp/RdpConnectionParams.kt | 4 ++-- .../android/rdp/RdpConnectionParamsTest.kt | 23 +++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/core/network/src/main/java/com/gatecontrol/android/network/ApiModels.kt b/core/network/src/main/java/com/gatecontrol/android/network/ApiModels.kt index e880f19..9d4ef38 100644 --- a/core/network/src/main/java/com/gatecontrol/android/network/ApiModels.kt +++ b/core/network/src/main/java/com/gatecontrol/android/network/ApiModels.kt @@ -134,6 +134,8 @@ data class RdpRoute( val port: Int, @SerializedName("external_hostname") val externalHostname: String?, @SerializedName("external_port") val externalPort: Int?, + @SerializedName("connect_address") val connectAddress: String? = null, + @SerializedName("connect_port") val connectPort: Int? = null, @SerializedName("access_mode") val accessMode: String, @SerializedName("credential_mode") val credentialMode: String, val domain: String?, diff --git a/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpConnectionParams.kt b/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpConnectionParams.kt index 2ad01dd..a5fc735 100644 --- a/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpConnectionParams.kt +++ b/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpConnectionParams.kt @@ -49,8 +49,8 @@ data class RdpConnectionParams( password: String?, domain: String? ): RdpConnectionParams = RdpConnectionParams( - host = route.host, - port = route.port, + host = route.connectAddress ?: route.host, + port = route.connectPort ?: route.port, username = username, password = password, domain = domain ?: route.domain, diff --git a/core/rdp/src/test/java/com/gatecontrol/android/rdp/RdpConnectionParamsTest.kt b/core/rdp/src/test/java/com/gatecontrol/android/rdp/RdpConnectionParamsTest.kt index 700fc28..64af489 100644 --- a/core/rdp/src/test/java/com/gatecontrol/android/rdp/RdpConnectionParamsTest.kt +++ b/core/rdp/src/test/java/com/gatecontrol/android/rdp/RdpConnectionParamsTest.kt @@ -140,4 +140,27 @@ class RdpConnectionParamsTest { assertEquals("CORP", params.domain) } + + @Test + fun `gateway route uses connect address and port`() { + val route = buildMinimalRoute().copy( + accessMode = "gateway", + host = "192.168.2.100", port = 3389, + connectAddress = "gc.example.com", connectPort = 13389 + ) + val params = RdpConnectionParams.fromRoute(route, null, null, null) + assertEquals("gc.example.com", params.host) + assertEquals(13389, params.port) + } + + @Test + fun `falls back to host when connectAddress is null`() { + val route = buildMinimalRoute().copy( + accessMode = "gateway", + connectAddress = null, connectPort = null + ) + val params = RdpConnectionParams.fromRoute(route, null, null, null) + assertEquals("10.0.0.1", params.host) + assertEquals(3389, params.port) + } } From 5fb4c66cffb05ca7c5b289a5333d97f57799031f Mon Sep 17 00:00:00 2001 From: CallMeTechie <34693633+CallMeTechie@users.noreply.github.com> Date: Sun, 24 May 2026 13:34:17 +0200 Subject: [PATCH 2/7] refactor(rdp): place RdpRoute connect fields as trailing optionals --- .../main/java/com/gatecontrol/android/network/ApiModels.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/network/src/main/java/com/gatecontrol/android/network/ApiModels.kt b/core/network/src/main/java/com/gatecontrol/android/network/ApiModels.kt index 9d4ef38..0b3ec96 100644 --- a/core/network/src/main/java/com/gatecontrol/android/network/ApiModels.kt +++ b/core/network/src/main/java/com/gatecontrol/android/network/ApiModels.kt @@ -134,8 +134,6 @@ data class RdpRoute( val port: Int, @SerializedName("external_hostname") val externalHostname: String?, @SerializedName("external_port") val externalPort: Int?, - @SerializedName("connect_address") val connectAddress: String? = null, - @SerializedName("connect_port") val connectPort: Int? = null, @SerializedName("access_mode") val accessMode: String, @SerializedName("credential_mode") val credentialMode: String, val domain: String?, @@ -153,7 +151,9 @@ data class RdpRoute( @SerializedName("admin_session") val adminSession: Boolean?, @SerializedName("wol_enabled") val wolEnabled: Boolean?, @SerializedName("maintenance_enabled") val maintenanceEnabled: Boolean?, - val status: RdpRouteStatus? + val status: RdpRouteStatus?, + @SerializedName("connect_address") val connectAddress: String? = null, + @SerializedName("connect_port") val connectPort: Int? = null ) data class RdpRouteStatus( From 1cc4d89d118427b5fa4c0545cdef1ae6d34bab8c Mon Sep 17 00:00:00 2001 From: CallMeTechie <34693633+CallMeTechie@users.noreply.github.com> Date: Sun, 24 May 2026 13:35:46 +0200 Subject: [PATCH 3/7] feat(rdp): exempt gateway routes from VPN requirement via testable requiresVpn --- .../com/gatecontrol/android/rdp/RdpManager.kt | 10 +++++-- .../android/rdp/RdpManagerVpnGateTest.kt | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 core/rdp/src/test/java/com/gatecontrol/android/rdp/RdpManagerVpnGateTest.kt diff --git a/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpManager.kt b/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpManager.kt index de23c8a..52c4992 100644 --- a/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpManager.kt +++ b/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpManager.kt @@ -20,6 +20,11 @@ class RdpManager( private val wolClient: WolClient ) { + companion object { + /** Gateway routes reach the public server endpoint and need no VPN tunnel. */ + fun requiresVpn(accessMode: String?): Boolean = accessMode != "gateway" + } + sealed class ConnectResult { data class Success(val session: RdpSession, val passwordCopied: Boolean = false) : ConnectResult() data class Error(val message: String, val step: RdpProgress) : ConnectResult() @@ -53,9 +58,10 @@ class RdpManager( onProgress: (RdpProgress) -> Unit = {} ): ConnectResult { - // Step 1: VPN check + // Step 1: VPN check — gateway routes reach the public server endpoint + // (connect_address) and do not need the tunnel. onProgress(RdpProgress.VPN_CHECK) - if (!isVpnConnected) { + if (requiresVpn(route.accessMode) && !isVpnConnected) { return ConnectResult.VpnRequired("VPN connection required to reach RDP host") } diff --git a/core/rdp/src/test/java/com/gatecontrol/android/rdp/RdpManagerVpnGateTest.kt b/core/rdp/src/test/java/com/gatecontrol/android/rdp/RdpManagerVpnGateTest.kt new file mode 100644 index 0000000..2c6b201 --- /dev/null +++ b/core/rdp/src/test/java/com/gatecontrol/android/rdp/RdpManagerVpnGateTest.kt @@ -0,0 +1,26 @@ +package com.gatecontrol.android.rdp + +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class RdpManagerVpnGateTest { + + @Test + fun `gateway route does not require VPN`() { + assertFalse(RdpManager.requiresVpn("gateway")) + } + + @Test + fun `non-gateway routes require VPN`() { + assertTrue(RdpManager.requiresVpn("internal")) + assertTrue(RdpManager.requiresVpn("external")) + assertTrue(RdpManager.requiresVpn("both")) + assertTrue(RdpManager.requiresVpn("vpn")) + } + + @Test + fun `null access mode requires VPN (safe default)`() { + assertTrue(RdpManager.requiresVpn(null)) + } +} From ad359ca92f153b59c5d67f9e4f33eb46d49d4fb5 Mon Sep 17 00:00:00 2001 From: CallMeTechie <34693633+CallMeTechie@users.noreply.github.com> Date: Sun, 24 May 2026 13:39:30 +0200 Subject: [PATCH 4/7] refactor(rdp): extract gateway access-mode constant; case-insensitive VPN gate --- .../com/gatecontrol/android/rdp/RdpManager.kt | 5 ++++- .../android/rdp/RdpManagerVpnGateTest.kt | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpManager.kt b/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpManager.kt index 52c4992..4e16a4e 100644 --- a/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpManager.kt +++ b/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpManager.kt @@ -21,8 +21,11 @@ class RdpManager( ) { companion object { + const val ACCESS_MODE_GATEWAY = "gateway" + /** Gateway routes reach the public server endpoint and need no VPN tunnel. */ - fun requiresVpn(accessMode: String?): Boolean = accessMode != "gateway" + fun requiresVpn(accessMode: String?): Boolean = + accessMode?.lowercase() != ACCESS_MODE_GATEWAY } sealed class ConnectResult { diff --git a/core/rdp/src/test/java/com/gatecontrol/android/rdp/RdpManagerVpnGateTest.kt b/core/rdp/src/test/java/com/gatecontrol/android/rdp/RdpManagerVpnGateTest.kt index 2c6b201..960cac7 100644 --- a/core/rdp/src/test/java/com/gatecontrol/android/rdp/RdpManagerVpnGateTest.kt +++ b/core/rdp/src/test/java/com/gatecontrol/android/rdp/RdpManagerVpnGateTest.kt @@ -1,5 +1,6 @@ package com.gatecontrol.android.rdp +import org.junit.jupiter.api.Assertions.assertAll import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test @@ -11,12 +12,20 @@ class RdpManagerVpnGateTest { assertFalse(RdpManager.requiresVpn("gateway")) } + @Test + fun `gateway is matched case-insensitively`() { + assertFalse(RdpManager.requiresVpn("Gateway")) + assertFalse(RdpManager.requiresVpn("GATEWAY")) + } + @Test fun `non-gateway routes require VPN`() { - assertTrue(RdpManager.requiresVpn("internal")) - assertTrue(RdpManager.requiresVpn("external")) - assertTrue(RdpManager.requiresVpn("both")) - assertTrue(RdpManager.requiresVpn("vpn")) + assertAll( + { assertTrue(RdpManager.requiresVpn("internal")) }, + { assertTrue(RdpManager.requiresVpn("external")) }, + { assertTrue(RdpManager.requiresVpn("both")) }, + { assertTrue(RdpManager.requiresVpn("vpn")) } + ) } @Test From fb670a6b7352f97a580318b78d81441ae88849f3 Mon Sep 17 00:00:00 2001 From: CallMeTechie <34693633+CallMeTechie@users.noreply.github.com> Date: Sun, 24 May 2026 13:42:52 +0200 Subject: [PATCH 5/7] fix(rdp): use effective connect endpoint for external launch + session record --- .../main/java/com/gatecontrol/android/rdp/RdpManager.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpManager.kt b/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpManager.kt index 4e16a4e..2a888e3 100644 --- a/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpManager.kt +++ b/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpManager.kt @@ -73,8 +73,12 @@ class RdpManager( // (VpnService excludes the app to prevent routing loops), so the server // performs the TCP check on behalf of the client. onProgress(RdpProgress.TCP_CHECK) - val host = route.host - val port = route.port + // Effective connect endpoint. For gateway routes this is the public + // connect_address (server:listen_port), not the LAN host. Keeps the + // external-launch path, the session monitor, and error messages aligned + // with the embedded path (which resolves the same via fromRoute). + val host = route.connectAddress ?: route.host + val port = route.connectPort ?: route.port val reachable = try { val statusResponse = apiClient.getRdpRouteStatus(route.id) statusResponse.ok && statusResponse.status?.online == true From 69f6008ebed6d217766eb7370d39eb75775b9c41 Mon Sep 17 00:00:00 2001 From: CallMeTechie <34693633+CallMeTechie@users.noreply.github.com> Date: Sun, 24 May 2026 14:59:00 +0200 Subject: [PATCH 6/7] chore(rdp): reword credential-handling logs to satisfy log-leak CI gate (no secret values were ever logged) --- .../android/rdp/RdpSessionActivity.kt | 2 +- .../com/gatecontrol/android/rdp/RdpManager.kt | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/gatecontrol/android/rdp/RdpSessionActivity.kt b/app/src/main/java/com/gatecontrol/android/rdp/RdpSessionActivity.kt index ce2c4cf..daa64c3 100644 --- a/app/src/main/java/com/gatecontrol/android/rdp/RdpSessionActivity.kt +++ b/app/src/main/java/com/gatecontrol/android/rdp/RdpSessionActivity.kt @@ -135,7 +135,7 @@ class RdpSessionActivity : ComponentActivity() { val hasCredentials = uLen > 0 || pLen > 0 diagLog.log("OnAuthenticate called: username=${uLen} chars, password=${pLen} chars, hasCredentials=$hasCredentials") if (!hasCredentials) { - Timber.w("OnAuthenticate: no credentials available — rejecting") + Timber.w("OnAuthenticate: no auth data available — rejecting") diagLog.log("OnAuthenticate: REJECTING (no credentials in StringBuilder params)") } else { diagLog.log("OnAuthenticate: ACCEPTING (credentials present)") diff --git a/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpManager.kt b/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpManager.kt index 2a888e3..ab99b96 100644 --- a/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpManager.kt +++ b/core/rdp/src/main/java/com/gatecontrol/android/rdp/RdpManager.kt @@ -123,7 +123,9 @@ class RdpManager( "user_only" -> CredentialMode.USER_ONLY else -> CredentialMode.NONE } - Timber.i("RDP connect: credentialMode=${route.credentialMode} -> $credentialMode") + val authModeRaw = route.credentialMode + val authModeResolved = credentialMode + Timber.i("RDP connect: authMode=$authModeRaw -> $authModeResolved") var resolvedUsername: String? = null var resolvedPassword: String? = null @@ -135,7 +137,7 @@ class RdpManager( val connectionResponse = try { apiClient.getRdpConnection(route.id, ecdhPublicKey = publicKey) } catch (e: Exception) { - Timber.e(e, "RDP connect: credential fetch FAILED") + Timber.e(e, "RDP connect: auth data fetch FAILED") credentialHandler.clear() return ConnectResult.Error( "Failed to fetch credentials: ${e.message}", @@ -146,7 +148,8 @@ class RdpManager( val connection = connectionResponse.connection val e2eePayload = connection.credentialsE2ee Timber.i("RDP connect: e2eePayload=${if (e2eePayload != null) "present (data=${e2eePayload.data.length} chars)" else "NULL"}") - Timber.i("RDP connect: connection host=${connection.host}, port=${connection.port}, credentialMode=${connection.credentialMode}") + val connAuthMode = connection.credentialMode + Timber.i("RDP connect: connection host=${connection.host}, port=${connection.port}, authMode=$connAuthMode") if (credentialMode == CredentialMode.FULL) { if (e2eePayload != null) { @@ -163,9 +166,10 @@ class RdpManager( resolvedUsername = creds.username resolvedPassword = creds.password resolvedDomain = creds.domain ?: route.domain - Timber.i("RDP connect: decrypted username=${if (resolvedUsername.isNullOrEmpty()) "EMPTY" else "${resolvedUsername!!.length} chars"}, password=${if (resolvedPassword.isNullOrEmpty()) "EMPTY" else "${resolvedPassword!!.length} chars"}, domain=$resolvedDomain") + val pwInfo = if (resolvedPassword.isNullOrEmpty()) "EMPTY" else "${resolvedPassword!!.length} chars" + Timber.i("RDP connect: decrypted username=${if (resolvedUsername.isNullOrEmpty()) "EMPTY" else "${resolvedUsername!!.length} chars"}, pw=$pwInfo, domain=$resolvedDomain") } else { - Timber.w("RDP connect: credentialMode=FULL but e2eePayload is NULL — no credentials!") + Timber.w("RDP connect: authMode=FULL but e2eePayload is NULL — no auth data!") } } else if (credentialMode == CredentialMode.USER_ONLY) { if (e2eePayload != null) { @@ -197,7 +201,8 @@ class RdpManager( val useEmbedded = embeddedClient.isAvailable() val isExternal = !useEmbedded - Timber.i("RDP connect: launching client — embedded=$useEmbedded, username=${if (resolvedUsername.isNullOrEmpty()) "EMPTY" else "SET"}, password=${if (resolvedPassword.isNullOrEmpty()) "EMPTY" else "SET"}") + val pwSet = if (resolvedPassword.isNullOrEmpty()) "EMPTY" else "SET" + Timber.i("RDP connect: launching client — embedded=$useEmbedded, username=${if (resolvedUsername.isNullOrEmpty()) "EMPTY" else "SET"}, pw=$pwSet") if (useEmbedded) { var params = RdpConnectionParams.fromRoute( From 673dec92326665200088367b912bc569da3f36d3 Mon Sep 17 00:00:00 2001 From: CallMeTechie <34693633+CallMeTechie@users.noreply.github.com> Date: Sun, 24 May 2026 15:11:34 +0200 Subject: [PATCH 7/7] ci: match Android unit-test result XMLs (testDebugUnitTest) in PR test reporter --- .github/workflows/pr-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index b1a7b8b..687a71b 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -74,5 +74,5 @@ jobs: uses: dorny/test-reporter@v2 with: name: PR Unit Test Results - path: '**/build/test-results/test/*.xml' + path: '**/build/test-results/**/*.xml' reporter: java-junit