diff --git a/android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt b/android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt index 338b7a9..a0b5c1b 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt @@ -4,9 +4,9 @@ package com.tailscale.ipn.ui.model import android.net.Uri -import java.util.UUID import kotlinx.serialization.Serializable import kotlinx.serialization.Transient +import java.util.UUID class Ipn { @@ -95,11 +95,11 @@ class Ipn { var ExitNodeIDSet: Boolean? = null, var ExitNodeAllowLANAccessSet: Boolean? = null, var WantRunningSet: Boolean? = null, + var LoggedOutSet: Boolean? = null, var ShieldsUpSet: Boolean? = null, var AdvertiseRoutesSet: Boolean? = null, var ForceDaemonSet: Boolean? = null, var HostnameSet: Boolean? = null, - var InternalExitNodePriorSet: Boolean? = null, ) { var ControlURL: String? = null @@ -126,12 +126,6 @@ class Ipn { ExitNodeIDSet = true } - var InternalExitNodePrior: String? = null - set(value) { - field = value - InternalExitNodePriorSet = true - } - var ExitNodeAllowLANAccess: Boolean? = null set(value) { field = value @@ -144,6 +138,12 @@ class Ipn { WantRunningSet = true } + var LoggedOut: Boolean? = null + set(value) { + field = value + LoggedOutSet = true + } + var ShieldsUp: Boolean? = null set(value) { field = value @@ -238,3 +238,20 @@ class Persist { var Provider: String = "", ) } + +fun Ipn.MaskedPrefs.deepCopy(): Ipn.MaskedPrefs { + return Ipn.MaskedPrefs().also { + if (this.ControlURLSet == true) it.ControlURL = this.ControlURL + if (this.RouteAllSet == true) it.RouteAll = this.RouteAll + if (this.CorpDNSSet == true) it.CorpDNS = this.CorpDNS + if (this.ExitNodeIDSet == true) it.ExitNodeID = this.ExitNodeID + if (this.ExitNodeAllowLANAccessSet == true) + it.ExitNodeAllowLANAccess = this.ExitNodeAllowLANAccess + if (this.WantRunningSet == true) it.WantRunning = this.WantRunning + if (this.LoggedOutSet == true) it.LoggedOut = this.LoggedOut + if (this.ShieldsUpSet == true) it.ShieldsUp = this.ShieldsUp + if (this.AdvertiseRoutesSet == true) it.AdvertiseRoutes = this.AdvertiseRoutes + if (this.ForceDaemonSet == true) it.ForceDaemon = this.ForceDaemon + if (this.HostnameSet == true) it.Hostname = this.Hostname + } +} diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt index 20230f6..39bf27f 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt @@ -11,6 +11,7 @@ import com.tailscale.ipn.ui.localapi.Client import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.IpnLocal import com.tailscale.ipn.ui.model.UserID +import com.tailscale.ipn.ui.model.deepCopy import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.ui.util.AdvertisedRoutesHelper import com.tailscale.ipn.ui.util.LoadingIndicator @@ -144,52 +145,54 @@ open class IpnViewModel : ViewModel() { // Login/Logout + /** + * Order of operations: + * 1. editPrefs() with maskedPrefs (to allow ControlURL override), WantRunning=true, LoggedOut=false if AuthKey != null + * 2. start() starts the LocalBackend state machine + * 3. startLoginInteractive() is currently required for bother interactive and non-interactive (using auth key) login + * + * Any failure short‑circuits the chain and invokes completionHandler once. + */ fun login( maskedPrefs: Ipn.MaskedPrefs? = null, authKey: String? = null, completionHandler: (Result) -> Unit = {} ) { + val client = Client(viewModelScope) - val loginAction = { - Client(viewModelScope).startLoginInteractive { result -> - result - .onSuccess { TSLog.d(TAG, "Login started: $it") } - .onFailure { TSLog.e(TAG, "Error starting login: ${it.message}") } - completionHandler(result) - } - } - - // Need to stop running before logging in to clear routes: - // https://linear.app/tailscale/issue/ENG-3441/routesdns-is-not-cleared-when-switching-profiles-or-reauthenticating - val stopThenLogin = { - Client(viewModelScope).editPrefs(Ipn.MaskedPrefs().apply { WantRunning = false }) { result -> - result - .onSuccess { loginAction() } - .onFailure { TSLog.e(TAG, "Error setting wantRunning to false: ${it.message}") } - } - } - - val startAction = { - Client(viewModelScope).start(Ipn.Options(AuthKey = authKey)) { start -> - start.onFailure { completionHandler(Result.failure(it)) }.onSuccess { stopThenLogin() } - } + val finalMaskedPrefs = maskedPrefs?.deepCopy() ?: Ipn.MaskedPrefs() + finalMaskedPrefs.WantRunning = true + if (authKey != null) { + finalMaskedPrefs.LoggedOut = false } - // If an MDM control URL is set, we will always use that in lieu of anything the user sets. - var prefs = maskedPrefs - val mdmControlURL = MDMSettings.loginURL.flow.value.value - - if (mdmControlURL != null) { - prefs = prefs ?: Ipn.MaskedPrefs() - prefs.ControlURL = mdmControlURL - TSLog.d(TAG, "Overriding control URL with MDM value: $mdmControlURL") + client.editPrefs(finalMaskedPrefs) { editResult -> + editResult + .onFailure { + TSLog.e(TAG, "editPrefs() failed: ${it.message}") + completionHandler(Result.failure(it)) + } + .onSuccess { + val opts = Ipn.Options(UpdatePrefs = editResult.getOrThrow(), AuthKey = authKey) + client.start(opts) { startResult -> + startResult + .onFailure { + TSLog.e(TAG, "start() failed: ${it.message}") + completionHandler(Result.failure(it)) + } + .onSuccess { + client.startLoginInteractive { loginResult -> + loginResult + .onFailure { + TSLog.e(TAG, "startLoginInteractive() failed: ${it.message}") + completionHandler(Result.failure(it)) + } + .onSuccess { completionHandler(Result.success(Unit)) } + } + } + } + } } - - prefs?.let { - Client(viewModelScope).editPrefs(it) { result -> - result.onFailure { completionHandler(Result.failure(it)) }.onSuccess { startAction() } - } - } ?: run { startAction() } } fun loginWithAuthKey(authKey: String, completionHandler: (Result) -> Unit = {}) {