android: exit node banner ui improvements (#408)

-show if device is running as an exit node
-show exit node connection status

Updates tailscale/corp#19122

Follow up will include:
-make exit node picker recompose when exit node connection status changes
-prevent user from running as exit node if it is using an exit node and vice versa instead of silently failing
-add explanation box for MDM offline state

Signed-off-by: kari-ts <kari@tailscale.com>
pull/412/head
kari-ts 1 month ago committed by GitHub
parent 72f35cd318
commit a05829b3c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -114,7 +114,13 @@ private val DarkColors =
) )
val ColorScheme.warning: Color val ColorScheme.warning: Color
get() = Color(0xFFD97916) // yellow-300 @Composable
get() =
if (isSystemInDarkTheme()) {
Color(0xFFBB5504) // yellow-400
} else {
Color(0xFFD97917) // yellow-300
}
val ColorScheme.onWarning: Color val ColorScheme.onWarning: Color
get() = Color(0xFFFFFFFF) // white get() = Color(0xFFFFFFFF) // white
@ -152,6 +158,15 @@ val ColorScheme.off: Color
val ColorScheme.link: Color val ColorScheme.link: Color
get() = onPrimaryContainer get() = onPrimaryContainer
val ColorScheme.customError: Color
@Composable
get() =
if (isSystemInDarkTheme()) {
Color(0xFF940821) // red-600
} else {
Color(0xFFB22D30) // red-500
}
/** /**
* Main color scheme for list items, uses onPrimaryContainer color for leading and trailing icons. * Main color scheme for list items, uses onPrimaryContainer color for leading and trailing icons.
*/ */
@ -256,6 +271,23 @@ val ColorScheme.warningListItem: ListItemColors
disabledTrailingIconColor = default.disabledTrailingIconColor) disabledTrailingIconColor = default.disabledTrailingIconColor)
} }
/** Color scheme for list items that should be styled as an error item. */
val ColorScheme.errorListItem: ListItemColors
@Composable
get() {
val default = ListItemDefaults.colors()
return ListItemColors(
containerColor = MaterialTheme.colorScheme.customError,
headlineColor = MaterialTheme.colorScheme.onPrimary,
leadingIconColor = MaterialTheme.colorScheme.onPrimary,
overlineColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f),
supportingTextColor = MaterialTheme.colorScheme.onPrimary,
trailingIconColor = MaterialTheme.colorScheme.onPrimary,
disabledHeadlineColor = default.disabledHeadlineColor,
disabledLeadingIconColor = default.disabledLeadingIconColor,
disabledTrailingIconColor = default.disabledTrailingIconColor)
}
/** Main color scheme for top app bar, styles it as a surface container. */ /** Main color scheme for top app bar, styles it as a surface container. */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
val ColorScheme.topAppBar: TopAppBarColors val ColorScheme.topAppBar: TopAppBarColors
@ -287,6 +319,44 @@ val ColorScheme.secondaryButton: ButtonColors
} }
} }
val ColorScheme.errorButton: ButtonColors
@Composable
get() {
val defaults = ButtonDefaults.buttonColors()
if (isSystemInDarkTheme()) {
return ButtonColors(
containerColor = Color(0xFFB22D30), // red-500
contentColor = Color(0xFFFFFFFF), // white
disabledContainerColor = defaults.disabledContainerColor,
disabledContentColor = defaults.disabledContentColor)
} else {
return ButtonColors(
containerColor = Color(0xFFD04841), // red-400
contentColor = Color(0xFFFFFFFF), // white
disabledContainerColor = defaults.disabledContainerColor,
disabledContentColor = defaults.disabledContentColor)
}
}
val ColorScheme.warningButton: ButtonColors
@Composable
get() {
val defaults = ButtonDefaults.buttonColors()
if (isSystemInDarkTheme()) {
return ButtonColors(
containerColor = Color(0xFFD97917), // yellow-300
contentColor = Color(0xFFFFFFFF), // white
disabledContainerColor = defaults.disabledContainerColor,
disabledContentColor = defaults.disabledContentColor)
} else {
return ButtonColors(
containerColor = Color(0xFFE5993E), // yellow-200
contentColor = Color(0xFFFFFFFF), // white
disabledContainerColor = defaults.disabledContainerColor,
disabledContentColor = defaults.disabledContentColor)
}
}
val ColorScheme.defaultTextColor: Color val ColorScheme.defaultTextColor: Color
@Composable @Composable
get() = get() =

@ -0,0 +1,24 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn.ui.util
import com.tailscale.ipn.ui.model.Ipn
class AdvertisedRoutesHelper {
companion object {
fun exitNodeOnFromPrefs(prefs: Ipn.Prefs): Boolean {
var v4 = false
var v6 = false
prefs.AdvertiseRoutes?.forEach {
if (it == "0.0.0.0/0") {
v4 = true
}
if (it == "::/0") {
v6 = true
}
}
return v4 && v6
}
}
}

@ -72,7 +72,8 @@ import com.tailscale.ipn.ui.model.Netmap
import com.tailscale.ipn.ui.model.Permissions import com.tailscale.ipn.ui.model.Permissions
import com.tailscale.ipn.ui.model.Tailcfg import com.tailscale.ipn.ui.model.Tailcfg
import com.tailscale.ipn.ui.theme.disabled import com.tailscale.ipn.ui.theme.disabled
import com.tailscale.ipn.ui.theme.exitNodeToggleButton import com.tailscale.ipn.ui.theme.errorButton
import com.tailscale.ipn.ui.theme.errorListItem
import com.tailscale.ipn.ui.theme.listItem import com.tailscale.ipn.ui.theme.listItem
import com.tailscale.ipn.ui.theme.minTextSize import com.tailscale.ipn.ui.theme.minTextSize
import com.tailscale.ipn.ui.theme.primaryListItem import com.tailscale.ipn.ui.theme.primaryListItem
@ -80,12 +81,12 @@ import com.tailscale.ipn.ui.theme.searchBarColors
import com.tailscale.ipn.ui.theme.secondaryButton import com.tailscale.ipn.ui.theme.secondaryButton
import com.tailscale.ipn.ui.theme.short import com.tailscale.ipn.ui.theme.short
import com.tailscale.ipn.ui.theme.surfaceContainerListItem import com.tailscale.ipn.ui.theme.surfaceContainerListItem
import com.tailscale.ipn.ui.theme.warningButton
import com.tailscale.ipn.ui.theme.warningListItem import com.tailscale.ipn.ui.theme.warningListItem
import com.tailscale.ipn.ui.util.AutoResizingText import com.tailscale.ipn.ui.util.AutoResizingText
import com.tailscale.ipn.ui.util.Lists import com.tailscale.ipn.ui.util.Lists
import com.tailscale.ipn.ui.util.LoadingIndicator import com.tailscale.ipn.ui.util.LoadingIndicator
import com.tailscale.ipn.ui.util.PeerSet import com.tailscale.ipn.ui.util.PeerSet
import com.tailscale.ipn.ui.util.flag
import com.tailscale.ipn.ui.util.itemsWithDividers import com.tailscale.ipn.ui.util.itemsWithDividers
import com.tailscale.ipn.ui.viewModel.MainViewModel import com.tailscale.ipn.ui.viewModel.MainViewModel
@ -202,10 +203,28 @@ fun MainView(
} }
} }
enum class NodeState {
NONE,
ACTIVE_AND_RUNNING,
// Last selected exit node is active but is not being used.
ACTIVE_NOT_RUNNING,
// Last selected exit node is currently offline.
OFFLINE_ENABLED,
// Last selected exit node has been de-selected and is currently offline.
OFFLINE_DISABLED,
// Exit node selection is managed by an administrator, and last selected exit node is currently
// offline
OFFLINE_MDM,
RUNNING_AS_EXIT_NODE
}
@Composable @Composable
fun ExitNodeStatus(navAction: () -> Unit, viewModel: MainViewModel) { fun ExitNodeStatus(navAction: () -> Unit, viewModel: MainViewModel) {
val maybePrefs by viewModel.prefs.collectAsState() val maybePrefs by viewModel.prefs.collectAsState()
val netmap by viewModel.netmap.collectAsState() val netmap by viewModel.netmap.collectAsState()
val isRunningExitNode by viewModel.isRunningExitNode.collectAsState()
var nodeState by remember { mutableStateOf(NodeState.NONE) }
// There's nothing to render if we haven't loaded the prefs yet // There's nothing to render if we haven't loaded the prefs yet
val prefs = maybePrefs ?: return val prefs = maybePrefs ?: return
@ -215,11 +234,36 @@ fun ExitNodeStatus(navAction: () -> Unit, viewModel: MainViewModel) {
val chosenExitNodeId = prefs.activeExitNodeID ?: prefs.selectedExitNodeID val chosenExitNodeId = prefs.activeExitNodeID ?: prefs.selectedExitNodeID
val exitNodePeer = chosenExitNodeId?.let { id -> netmap?.Peers?.find { it.StableID == id } } val exitNodePeer = chosenExitNodeId?.let { id -> netmap?.Peers?.find { it.StableID == id } }
val location = exitNodePeer?.Hostinfo?.Location
val name = exitNodePeer?.ComputedName val name = exitNodePeer?.ComputedName
// We're connected to an exit node if we found an active peer for the *active* exit node val online = exitNodePeer?.Online
val activeAndRunning = (exitNodePeer != null) && !prefs.activeExitNodeID.isNullOrEmpty()
LaunchedEffect(prefs.ExitNodeID, exitNodePeer?.Online, isRunningExitNode) {
when {
exitNodePeer?.Online == false -> {
if (MDMSettings.exitNodeID.flow.value != null) {
nodeState = NodeState.OFFLINE_MDM
} else if (prefs.activeExitNodeID != null) {
nodeState = NodeState.OFFLINE_ENABLED
} else {
nodeState = NodeState.OFFLINE_DISABLED
}
}
exitNodePeer != null -> {
if (!prefs.activeExitNodeID.isNullOrEmpty()) {
nodeState = NodeState.ACTIVE_AND_RUNNING
} else {
nodeState = NodeState.ACTIVE_NOT_RUNNING
}
}
isRunningExitNode -> {
nodeState = NodeState.RUNNING_AS_EXIT_NODE
}
else -> {
nodeState = NodeState.NONE
}
}
}
// (jonathan) TODO: We will block the "enable/disable" button for an exit node for which we cannot // (jonathan) TODO: We will block the "enable/disable" button for an exit node for which we cannot
// find a peer on purpose and render the "No Exit Node" state, however, that should // find a peer on purpose and render the "No Exit Node" state, however, that should
@ -234,11 +278,24 @@ fun ExitNodeStatus(navAction: () -> Unit, viewModel: MainViewModel) {
ListItem( ListItem(
modifier = Modifier.clickable { navAction() }, modifier = Modifier.clickable { navAction() },
colors = colors =
if (activeAndRunning) MaterialTheme.colorScheme.primaryListItem when (nodeState) {
else ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surface), NodeState.ACTIVE_AND_RUNNING -> MaterialTheme.colorScheme.primaryListItem
NodeState.ACTIVE_NOT_RUNNING -> MaterialTheme.colorScheme.primaryListItem
NodeState.RUNNING_AS_EXIT_NODE -> MaterialTheme.colorScheme.warningListItem
NodeState.OFFLINE_ENABLED -> MaterialTheme.colorScheme.errorListItem
NodeState.OFFLINE_DISABLED -> MaterialTheme.colorScheme.errorListItem
NodeState.OFFLINE_MDM -> MaterialTheme.colorScheme.errorListItem
else ->
ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surface)
},
overlineContent = { overlineContent = {
Text( Text(
stringResource(R.string.exit_node), text =
if (nodeState == NodeState.OFFLINE_ENABLED ||
nodeState == NodeState.OFFLINE_DISABLED ||
nodeState == NodeState.OFFLINE_MDM)
stringResource(R.string.exit_node_offline)
else stringResource(R.string.exit_node),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
) )
}, },
@ -246,9 +303,12 @@ fun ExitNodeStatus(navAction: () -> Unit, viewModel: MainViewModel) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
text = text =
location?.let { "${it.CountryCode?.flag()} ${it.Country}: ${it.City}" } when (nodeState) {
?: name NodeState.NONE -> stringResource(id = R.string.none)
?: stringResource(id = R.string.none), NodeState.RUNNING_AS_EXIT_NODE ->
stringResource(id = R.string.running_exit_node)
else -> name ?: ""
},
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis) overflow = TextOverflow.Ellipsis)
@ -256,24 +316,36 @@ fun ExitNodeStatus(navAction: () -> Unit, viewModel: MainViewModel) {
imageVector = Icons.Outlined.ArrowDropDown, imageVector = Icons.Outlined.ArrowDropDown,
contentDescription = null, contentDescription = null,
tint = tint =
if (activeAndRunning) if (nodeState == NodeState.NONE)
MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f) MaterialTheme.colorScheme.onSurfaceVariant
else MaterialTheme.colorScheme.onSurfaceVariant, else MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f),
) )
} }
}, },
trailingContent = { trailingContent = {
if (exitNodePeer != null) { if (nodeState != NodeState.NONE) {
Button( Button(
colors = colors =
if (prefs.activeExitNodeID.isNullOrEmpty()) when (nodeState) {
MaterialTheme.colorScheme.exitNodeToggleButton NodeState.OFFLINE_ENABLED -> MaterialTheme.colorScheme.errorButton
else MaterialTheme.colorScheme.secondaryButton, NodeState.OFFLINE_DISABLED -> MaterialTheme.colorScheme.errorButton
onClick = { viewModel.toggleExitNode() }) { NodeState.OFFLINE_MDM -> MaterialTheme.colorScheme.errorButton
NodeState.RUNNING_AS_EXIT_NODE ->
MaterialTheme.colorScheme.warningButton
else -> MaterialTheme.colorScheme.secondaryButton
},
onClick = {
if (nodeState == NodeState.RUNNING_AS_EXIT_NODE)
viewModel.setRunningExitNode(false)
else viewModel.toggleExitNode()
}) {
Text( Text(
if (prefs.activeExitNodeID.isNullOrEmpty()) when (nodeState) {
stringResource(id = R.string.enable) NodeState.OFFLINE_DISABLED -> stringResource(id = R.string.enable)
else stringResource(id = R.string.disable)) NodeState.ACTIVE_NOT_RUNNING -> stringResource(id = R.string.enable)
NodeState.RUNNING_AS_EXIT_NODE -> stringResource(id = R.string.stop)
else -> stringResource(id = R.string.disable)
})
} }
} }
}) })

@ -32,14 +32,10 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.tailscale.ipn.R import com.tailscale.ipn.R
import com.tailscale.ipn.ui.util.LoadingIndicator import com.tailscale.ipn.ui.util.LoadingIndicator
import com.tailscale.ipn.ui.viewModel.ExitNodePickerNav import com.tailscale.ipn.ui.viewModel.ExitNodePickerNav
import com.tailscale.ipn.ui.viewModel.RunExitNodeViewModel import com.tailscale.ipn.ui.viewModel.IpnViewModel
import com.tailscale.ipn.ui.viewModel.RunExitNodeViewModelFactory
@Composable @Composable
fun RunExitNodeView( fun RunExitNodeView(nav: ExitNodePickerNav, model: IpnViewModel = viewModel()) {
nav: ExitNodePickerNav,
model: RunExitNodeViewModel = viewModel(factory = RunExitNodeViewModelFactory())
) {
val isRunningExitNode by model.isRunningExitNode.collectAsState() val isRunningExitNode by model.isRunningExitNode.collectAsState()
Scaffold( Scaffold(

@ -54,7 +54,6 @@ class ExitNodePickerViewModel(private val nav: ExitNodePickerNav) : IpnViewModel
val mullvadBestAvailableByCountry: StateFlow<Map<String, ExitNode>> = MutableStateFlow(TreeMap()) val mullvadBestAvailableByCountry: StateFlow<Map<String, ExitNode>> = MutableStateFlow(TreeMap())
val mullvadExitNodeCount: StateFlow<Int> = MutableStateFlow(0) val mullvadExitNodeCount: StateFlow<Int> = MutableStateFlow(0)
val anyActive: StateFlow<Boolean> = MutableStateFlow(false) val anyActive: StateFlow<Boolean> = MutableStateFlow(false)
val isRunningExitNode: StateFlow<Boolean> = MutableStateFlow(false)
init { init {
viewModelScope.launch { viewModelScope.launch {
@ -62,7 +61,6 @@ class ExitNodePickerViewModel(private val nav: ExitNodePickerNav) : IpnViewModel
.combine(Notifier.prefs) { netmap, prefs -> Pair(netmap, prefs) } .combine(Notifier.prefs) { netmap, prefs -> Pair(netmap, prefs) }
.stateIn(viewModelScope) .stateIn(viewModelScope)
.collect { (netmap, prefs) -> .collect { (netmap, prefs) ->
isRunningExitNode.set(prefs?.let { AdvertisedRoutesHelper.exitNodeOnFromPrefs(it) })
val exitNodeId = prefs?.activeExitNodeID ?: prefs?.selectedExitNodeID val exitNodeId = prefs?.activeExitNodeID ?: prefs?.selectedExitNodeID
netmap?.Peers?.let { peers -> netmap?.Peers?.let { peers ->
val allNodes = val allNodes =

@ -16,6 +16,7 @@ import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.model.UserID import com.tailscale.ipn.ui.model.UserID
import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.notifier.Notifier.prefs import com.tailscale.ipn.ui.notifier.Notifier.prefs
import com.tailscale.ipn.ui.util.AdvertisedRoutesHelper
import com.tailscale.ipn.ui.util.LoadingIndicator import com.tailscale.ipn.ui.util.LoadingIndicator
import com.tailscale.ipn.ui.util.set import com.tailscale.ipn.ui.util.set
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -31,12 +32,16 @@ open class IpnViewModel : ViewModel() {
val loggedInUser: StateFlow<IpnLocal.LoginProfile?> = MutableStateFlow(null) val loggedInUser: StateFlow<IpnLocal.LoginProfile?> = MutableStateFlow(null)
val loginProfiles: StateFlow<List<IpnLocal.LoginProfile>?> = MutableStateFlow(null) val loginProfiles: StateFlow<List<IpnLocal.LoginProfile>?> = MutableStateFlow(null)
private val _vpnPrepared = MutableStateFlow(false) private val _vpnPrepared = MutableStateFlow(false)
val vpnPrepared: StateFlow<Boolean> = _vpnPrepared val vpnPrepared: StateFlow<Boolean> = _vpnPrepared
// The userId associated with the current node. ie: The logged in user. // The userId associated with the current node. ie: The logged in user.
private var selfNodeUserId: UserID? = null private var selfNodeUserId: UserID? = null
val isRunningExitNode: StateFlow<Boolean> = MutableStateFlow(false)
var lastPrefs: Ipn.Prefs? = null
init { init {
// Check if the user has granted permission yet. // Check if the user has granted permission yet.
if (!vpnPrepared.value) { if (!vpnPrepared.value) {
@ -68,11 +73,19 @@ open class IpnViewModel : ViewModel() {
} }
} }
viewModelScope.launch {
Notifier.prefs.collect {
it?.let { lastPrefs = it }
isRunningExitNode.set(it?.let { AdvertisedRoutesHelper.exitNodeOnFromPrefs(it) })
}
}
viewModelScope.launch { loadUserProfiles() } viewModelScope.launch { loadUserProfiles() }
Log.d(TAG, "Created") Log.d(TAG, "Created")
} }
// VPN Control // VPN Control
fun setVpnPrepared(prepared: Boolean) { fun setVpnPrepared(prepared: Boolean) {
_vpnPrepared.value = prepared _vpnPrepared.value = prepared
} }
@ -223,4 +236,41 @@ open class IpnViewModel : ViewModel() {
Log.e(TAG, "No exit node to disable and no prior exit node to enable") Log.e(TAG, "No exit node to disable and no prior exit node to enable")
} }
} }
fun setRunningExitNode(isOn: Boolean) {
LoadingIndicator.start()
lastPrefs?.let { currentPrefs ->
val newPrefs: Ipn.MaskedPrefs
if (isOn) {
newPrefs = setZeroRoutes(currentPrefs)
} else {
newPrefs = removeAllZeroRoutes(currentPrefs)
}
Client(viewModelScope).editPrefs(newPrefs) { result ->
LoadingIndicator.stop()
Log.d("RunExitNodeViewModel", "Edited prefs: $result")
}
}
}
private fun setZeroRoutes(prefs: Ipn.Prefs): Ipn.MaskedPrefs {
val newRoutes = (removeAllZeroRoutes(prefs).AdvertiseRoutes ?: emptyList()).toMutableList()
newRoutes.add("0.0.0.0/0")
newRoutes.add("::/0")
val newPrefs = Ipn.MaskedPrefs()
newPrefs.AdvertiseRoutes = newRoutes
return newPrefs
}
private fun removeAllZeroRoutes(prefs: Ipn.Prefs): Ipn.MaskedPrefs {
val newRoutes = emptyList<String>().toMutableList()
(prefs.AdvertiseRoutes ?: emptyList()).forEach {
if (it != "0.0.0.0/0" && it != "::/0") {
newRoutes.add(it)
}
}
val newPrefs = Ipn.MaskedPrefs()
newPrefs.AdvertiseRoutes = newRoutes
return newPrefs
}
} }

@ -1,97 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn.ui.viewModel
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.tailscale.ipn.ui.localapi.Client
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.util.LoadingIndicator
import com.tailscale.ipn.ui.util.set
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class RunExitNodeViewModelFactory() : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return RunExitNodeViewModel() as T
}
}
class AdvertisedRoutesHelper() {
companion object {
fun exitNodeOnFromPrefs(prefs: Ipn.Prefs): Boolean {
var v4 = false
var v6 = false
prefs.AdvertiseRoutes?.forEach {
if (it == "0.0.0.0/0") {
v4 = true
}
if (it == "::/0") {
v6 = true
}
}
return v4 && v6
}
}
}
class RunExitNodeViewModel() : IpnViewModel() {
val isRunningExitNode: StateFlow<Boolean> = MutableStateFlow(false)
var lastPrefs: Ipn.Prefs? = null
init {
viewModelScope.launch {
Notifier.prefs.stateIn(viewModelScope).collect { prefs ->
Log.d("RunExitNode", "prefs: AdvertiseRoutes=" + prefs?.AdvertiseRoutes.toString())
prefs?.let {
lastPrefs = it
isRunningExitNode.set(AdvertisedRoutesHelper.exitNodeOnFromPrefs(it))
} ?: run { isRunningExitNode.set(false) }
}
}
}
fun setRunningExitNode(isOn: Boolean) {
LoadingIndicator.start()
lastPrefs?.let { currentPrefs ->
val newPrefs: Ipn.MaskedPrefs
if (isOn) {
newPrefs = setZeroRoutes(currentPrefs)
} else {
newPrefs = removeAllZeroRoutes(currentPrefs)
}
Client(viewModelScope).editPrefs(newPrefs) { result ->
LoadingIndicator.stop()
Log.d("RunExitNodeViewModel", "Edited prefs: $result")
}
}
}
private fun setZeroRoutes(prefs: Ipn.Prefs): Ipn.MaskedPrefs {
val newRoutes = (removeAllZeroRoutes(prefs).AdvertiseRoutes ?: emptyList()).toMutableList()
newRoutes.add("0.0.0.0/0")
newRoutes.add("::/0")
val newPrefs = Ipn.MaskedPrefs()
newPrefs.AdvertiseRoutes = newRoutes
return newPrefs
}
private fun removeAllZeroRoutes(prefs: Ipn.Prefs): Ipn.MaskedPrefs {
val newRoutes = emptyList<String>().toMutableList()
(prefs.AdvertiseRoutes ?: emptyList()).forEach {
if (it != "0.0.0.0/0" && it != "::/0") {
newRoutes.add(it)
}
}
val newPrefs = Ipn.MaskedPrefs()
newPrefs.AdvertiseRoutes = newRoutes
return newPrefs
}
}

@ -51,6 +51,8 @@
<!-- Strings for the main screen --> <!-- Strings for the main screen -->
<string name="exit_node">EXIT NODE</string> <string name="exit_node">EXIT NODE</string>
<string name="exit_node_offline">EXIT NODE OFFLINE</string>
<string name="running_exit_node">Running on this device</string>
<string name="starting">Starting…</string> <string name="starting">Starting…</string>
<string name="connect_to_tailnet_prefix">"Connect again to talk to the other devices in the "</string> <string name="connect_to_tailnet_prefix">"Connect again to talk to the other devices in the "</string>
<string name="connect_to_tailnet_suffix">" tailnet."</string> <string name="connect_to_tailnet_suffix">" tailnet."</string>
@ -67,6 +69,7 @@
<string name="needs_machine_auth">Authorization required</string> <string name="needs_machine_auth">Authorization required</string>
<string name="disable">Disable</string> <string name="disable">Disable</string>
<string name="enable">Enable</string> <string name="enable">Enable</string>
<string name="stop">Stop</string>
<!-- Strings for peer details --> <!-- Strings for peer details -->
<string name="os">OS</string> <string name="os">OS</string>

Loading…
Cancel
Save