oxtoacart/reactive_exit_node_bak
Percy Wegmann 3 months ago
parent a1e67ff1e9
commit dfd43a4195
No known key found for this signature in database
GPG Key ID: 29D8CDEB4C13D48B

@ -8,7 +8,6 @@ import com.tailscale.ipn.ui.model.BugReportID
import com.tailscale.ipn.ui.model.Errors import com.tailscale.ipn.ui.model.Errors
import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.IpnLocal import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.model.IpnState
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -44,7 +43,6 @@ private object Endpoint {
const val TAILFS_SERVER_ADDRESS = "tailfs/fileserver-address" const val TAILFS_SERVER_ADDRESS = "tailfs/fileserver-address"
} }
typealias StatusResponseHandler = (Result<IpnState.Status>) -> Unit
typealias BugReportIdHandler = (Result<BugReportID>) -> Unit typealias BugReportIdHandler = (Result<BugReportID>) -> Unit
typealias PrefsHandler = (Result<Ipn.Prefs>) -> Unit typealias PrefsHandler = (Result<Ipn.Prefs>) -> Unit
@ -53,10 +51,6 @@ typealias PrefsHandler = (Result<Ipn.Prefs>) -> Unit
* corresponding method on this Client. * corresponding method on this Client.
*/ */
class Client(private val scope: CoroutineScope) { class Client(private val scope: CoroutineScope) {
fun status(responseHandler: StatusResponseHandler) {
get(Endpoint.STATUS, responseHandler = responseHandler)
}
fun bugReportId(responseHandler: BugReportIdHandler) { fun bugReportId(responseHandler: BugReportIdHandler) {
post(Endpoint.BUG_REPORT, responseHandler = responseHandler) post(Endpoint.BUG_REPORT, responseHandler = responseHandler)
} }

@ -1,30 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn.ui.model
import kotlinx.serialization.Serializable
class Dns {
@Serializable
data class HostEntry(val addr: Addr?, val hosts: List<String>?)
@Serializable
data class OSConfig(
val hosts: List<HostEntry>? = null,
val nameservers: List<Addr>? = null,
val searchDomains: List<String>? = null,
val matchDomains: List<String>? = null,
) {
val isEmpty: Boolean
get() = (hosts.isNullOrEmpty()) &&
(nameservers.isNullOrEmpty()) &&
(searchDomains.isNullOrEmpty()) &&
(matchDomains.isNullOrEmpty())
}
}
class DnsType {
@Serializable
data class Resolver(var Addr: String? = null, var BootstrapResolution: List<Addr>? = null)
}

@ -9,13 +9,9 @@ class Ipn {
// Represents the overall state of the Tailscale engine. // Represents the overall state of the Tailscale engine.
enum class State(val value: Int) { enum class State(val value: Int) {
NoState(0), NoState(0), InUseOtherUser(1), NeedsLogin(2), NeedsMachineAuth(3), Stopped(4), Starting(5), Running(
InUseOtherUser(1), 6
NeedsLogin(2), );
NeedsMachineAuth(3),
Stopped(4),
Starting(5),
Running(6);
companion object { companion object {
fun fromInt(value: Int): State { fun fromInt(value: Int): State {
@ -28,20 +24,20 @@ class Ipn {
// on which NotifyWatchOpts were set when the Notifier was created. // on which NotifyWatchOpts were set when the Notifier was created.
@Serializable @Serializable
data class Notify( data class Notify(
val Version: String? = null, val Version: String? = null,
val ErrMessage: String? = null, val ErrMessage: String? = null,
val LoginFinished: Empty.Message? = null, val LoginFinished: Empty.Message? = null,
val FilesWaiting: Empty.Message? = null, val FilesWaiting: Empty.Message? = null,
val State: Int? = null, val State: Int? = null,
var Prefs: Prefs? = null, var Prefs: Prefs? = null,
var NetMap: Netmap.NetworkMap? = null, var NetMap: NetworkMap? = null,
var Engine: EngineStatus? = null, var Engine: EngineStatus? = null,
var BrowseToURL: String? = null, var BrowseToURL: String? = null,
var BackendLogId: String? = null, var BackendLogId: String? = null,
var LocalTCPPort: Int? = null, var LocalTCPPort: Int? = null,
var IncomingFiles: List<PartialFile>? = null, var IncomingFiles: List<PartialFile>? = null,
var ClientVersion: Tailcfg.ClientVersion? = null, // var ClientVersion: Tailcfg.ClientVersion? = null, // Currently unused
var TailFSShares: Map<String, String>? = null, var TailFSShares: Map<String, String>? = null,
) )
@Serializable @Serializable
@ -65,15 +61,15 @@ class Ipn {
@Serializable @Serializable
data class MaskedPrefs( data class MaskedPrefs(
var RouteAllSet: Boolean? = null, var RouteAllSet: Boolean? = null,
var CorpDNSSet: Boolean? = null, var CorpDNSSet: Boolean? = null,
var ExitNodeIDSet: Boolean? = null, var ExitNodeIDSet: Boolean? = null,
var ExitNodeAllowLANAccessSet: Boolean? = null, var ExitNodeAllowLANAccessSet: Boolean? = null,
var WantRunningSet: Boolean? = null, var WantRunningSet: Boolean? = null,
var ShieldsUpSet: Boolean? = null, var ShieldsUpSet: Boolean? = null,
var AdvertiseRoutesSet: Boolean? = null, var AdvertiseRoutesSet: Boolean? = null,
var ForceDaemonSet: Boolean? = null, var ForceDaemonSet: Boolean? = null,
var HostnameSet: Boolean? = null, var HostnameSet: Boolean? = null,
) { ) {
var RouteAll: Boolean? = null var RouteAll: Boolean? = null
set(value) { set(value) {
@ -124,39 +120,36 @@ class Ipn {
@Serializable @Serializable
data class AutoUpdatePrefs( data class AutoUpdatePrefs(
var Check: Boolean? = null, var Check: Boolean? = null,
var Apply: Boolean? = null, var Apply: Boolean? = null,
) )
@Serializable @Serializable
data class EngineStatus( data class EngineStatus(
val RBytes: Long, val RBytes: Long,
val WBytes: Long, val WBytes: Long,
val NumLive: Int, val NumLive: Int,
val LivePeers: Map<String, IpnState.PeerStatusLite>, // val LivePeers: Map<String, IpnState.PeerStatusLite>,
) )
@Serializable @Serializable
data class PartialFile( data class PartialFile(
val Name: String, val Name: String,
val Started: String, val Started: String,
val DeclaredSize: Long, val DeclaredSize: Long,
val Received: Long, val Received: Long,
val PartialPath: String? = null, val PartialPath: String? = null,
var FinalPath: String? = null, var FinalPath: String? = null,
val Done: Boolean? = null, val Done: Boolean? = null,
) )
} }
class Persist { class Persist {
@Serializable @Serializable
data class Persist( data class Persist(
var PrivateMachineKey: String = var PrivateMachineKey: String = "privkey:0000000000000000000000000000000000000000000000000000000000000000",
"privkey:0000000000000000000000000000000000000000000000000000000000000000", var PrivateNodeKey: String = "privkey:0000000000000000000000000000000000000000000000000000000000000000",
var PrivateNodeKey: String = var OldPrivateNodeKey: String = "privkey:0000000000000000000000000000000000000000000000000000000000000000",
"privkey:0000000000000000000000000000000000000000000000000000000000000000", var Provider: String = "",
var OldPrivateNodeKey: String =
"privkey:0000000000000000000000000000000000000000000000000000000000000000",
var Provider: String = "",
) )
} }

@ -5,114 +5,15 @@ package com.tailscale.ipn.ui.model
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
class IpnState {
@Serializable
data class PeerStatusLite(
val RxBytes: Long,
val TxBytes: Long,
val LastHandshake: String,
val NodeKey: String,
)
@Serializable
data class PeerStatus(
val ID: StableNodeID,
val HostName: String,
val DNSName: String,
val TailscaleIPs: List<Addr>? = null,
val Tags: List<String>? = null,
val PrimaryRoutes: List<String>? = null,
val Addrs: List<String>? = null,
val Online: Boolean,
val ExitNode: Boolean,
val ExitNodeOption: Boolean,
val Active: Boolean,
val PeerAPIURL: List<String>? = null,
val Capabilities: List<String>? = null,
val SSH_HostKeys: List<String>? = null,
val ShareeNode: Boolean? = null,
val Expired: Boolean? = null,
val Location: Tailcfg.Location? = null,
) {
fun computedName(status: Status): String {
val name = DNSName
val suffix = status.CurrentTailnet?.MagicDNSSuffix
suffix ?: return name
if (!(name.endsWith("." + suffix + "."))) {
return name
}
return name.dropLast(suffix.count() + 2)
}
}
@Serializable
data class ExitNodeStatus(
val ID: StableNodeID,
val Online: Boolean,
val TailscaleIPs: List<Prefix>? = null,
)
@Serializable
data class TailnetStatus(
val Name: String,
val MagicDNSSuffix: String,
val MagicDNSEnabled: Boolean,
)
@Serializable
data class Status(
val Version: String,
val TUN: Boolean,
val BackendState: String,
val AuthURL: String,
val TailscaleIPs: List<Addr>? = null,
val Self: PeerStatus? = null,
val ExitNodeStatus: ExitNodeStatus? = null,
val Health: List<String>? = null,
val CurrentTailnet: TailnetStatus? = null,
val CertDomains: List<String>? = null,
val Peer: Map<String, PeerStatus>? = null,
val User: Map<String, Tailcfg.UserProfile>? = null,
val ClientVersion: Tailcfg.ClientVersion? = null,
)
@Serializable
data class NetworkLockStatus(
var Enabled: Boolean,
var PublicKey: String,
var NodeKey: String,
var NodeKeySigned: Boolean,
var FilteredPeers: List<TKAFilteredPeer>? = null,
var StateID: ULong? = null,
)
@Serializable
data class TKAFilteredPeer(
var Name: String,
var TailscaleIPs: List<Addr>,
var NodeKey: String,
)
@Serializable
data class PingResult(
var IP: Addr,
var Err: String,
var LatencySeconds: Double,
)
}
class IpnLocal { class IpnLocal {
@Serializable @Serializable
data class LoginProfile( data class LoginProfile(
var ID: String, var ID: String,
val Name: String, val Name: String,
val Key: String, val Key: String,
val UserProfile: Tailcfg.UserProfile, val UserProfile: Tailcfg.UserProfile,
val NetworkProfile: Tailcfg.NetworkProfile? = null, val NetworkProfile: Tailcfg.NetworkProfile? = null,
val LocalUserID: String, val LocalUserID: String,
) { ) {
fun isEmpty(): Boolean { fun isEmpty(): Boolean {
return ID.isEmpty() return ID.isEmpty()

@ -5,51 +5,49 @@ package com.tailscale.ipn.ui.model
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
class Netmap { @Serializable
@Serializable data class NetworkMap(
data class NetworkMap( var SelfNode: Tailcfg.Node,
var SelfNode: Tailcfg.Node, // var NodeKey: KeyNodePublic, // Currently unused
var NodeKey: KeyNodePublic, var Peers: List<Tailcfg.Node>? = null,
var Peers: List<Tailcfg.Node>? = null, // var Expiry: Time, // Currently unused
var Expiry: Time, // var Domain: String, // Currently unused
var Domain: String, var UserProfiles: Map<String, Tailcfg.UserProfile>,
var UserProfiles: Map<String, Tailcfg.UserProfile>, // var TKAEnabled: Boolean, // Currently unused
var TKAEnabled: Boolean, // var DNS: Tailcfg.DNSConfig? = null // Currently unused
var DNS: Tailcfg.DNSConfig? = null ) {
) { // Keys are tailcfg.UserIDs thet get stringified
// Keys are tailcfg.UserIDs thet get stringified // Helpers
// Helpers fun currentUserProfile(): Tailcfg.UserProfile? {
fun currentUserProfile(): Tailcfg.UserProfile? { return userProfile(User())
return userProfile(User()) }
}
fun User(): UserID { fun User(): UserID {
return SelfNode.User return SelfNode.User
} }
fun userProfile(id: Long): Tailcfg.UserProfile? { fun userProfile(id: Long): Tailcfg.UserProfile? {
return UserProfiles[id.toString()] return UserProfiles[id.toString()]
} }
fun getPeer(id: StableNodeID): Tailcfg.Node? { fun getPeer(id: StableNodeID): Tailcfg.Node? {
if(id == SelfNode.StableID) { if (id == SelfNode.StableID) {
return SelfNode return SelfNode
}
return Peers?.find { it.StableID == id }
} }
return Peers?.find { it.StableID == id }
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is NetworkMap) return false if (other !is NetworkMap) return false
return SelfNode == other.SelfNode && return SelfNode == other.SelfNode &&
NodeKey == other.NodeKey && // NodeKey == other.NodeKey &&
Peers == other.Peers && Peers == other.Peers &&
Expiry == other.Expiry && // Expiry == other.Expiry &&
User() == other.User() && User() == other.User() &&
Domain == other.Domain && // Domain == other.Domain &&
UserProfiles == other.UserProfiles && UserProfiles == other.UserProfiles
TKAEnabled == other.TKAEnabled // TKAEnabled == other.TKAEnabled
}
} }
} }

@ -6,22 +6,23 @@ package com.tailscale.ipn.ui.model
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
class Tailcfg { class Tailcfg {
@Serializable // Currently unused
data class ClientVersion( // @Serializable
var RunningLatest: Boolean? = null, // data class ClientVersion(
var LatestVersion: String? = null, // var RunningLatest: Boolean? = null,
var UrgentSecurityUpdate: Boolean? = null, // var LatestVersion: String? = null,
var Notify: Boolean? = null, // var UrgentSecurityUpdate: Boolean? = null,
var NotifyURL: String? = null, // var Notify: Boolean? = null,
var NotifyText: String? = null // var NotifyURL: String? = null,
) // var NotifyText: String? = null
// )
@Serializable @Serializable
data class UserProfile( data class UserProfile(
val ID: Long, val ID: Long,
val DisplayName: String, val DisplayName: String,
val LoginName: String, val LoginName: String,
val ProfilePicURL: String? = null, val ProfilePicURL: String? = null,
) { ) {
fun isTaggedDevice(): Boolean { fun isTaggedDevice(): Boolean {
return LoginName == "tagged-devices" return LoginName == "tagged-devices"
@ -30,74 +31,80 @@ class Tailcfg {
@Serializable @Serializable
data class Hostinfo( data class Hostinfo(
var IPNVersion: String? = null, // var IPNVersion: String? = null, // Currently unused
var FrontendLogID: String? = null, // var FrontendLogID: String? = null, // Currently unused
var BackendLogID: String? = null, // var BackendLogID: String? = null, // Currently unused
var OS: String? = null, var OS: String? = null,
var OSVersion: String? = null, // var OSVersion: String? = null, // Currently unused
var Env: String? = null, // var Env: String? = null, // Currently unused
var Distro: String? = null, // var Distro: String? = null, // Currently unused
var DistroVersion: String? = null, // var DistroVersion: String? = null, // Currently unused
var DistroCodeName: String? = null, // var DistroCodeName: String? = null, // Currently unused
var Desktop: Boolean? = null, // var Desktop: Boolean? = null, // Currently unused
var Package: String? = null, // var Package: String? = null, // Currently unused
var DeviceModel: String? = null, // var DeviceModel: String? = null, // Currently unused
var ShareeNode: Boolean? = null, // var ShareeNode: Boolean? = null, // Currently unused
var Hostname: String? = null, // var Hostname: String? = null, // Currently unused
var ShieldsUp: Boolean? = null, // var ShieldsUp: Boolean? = null, // Currently unused
var NoLogsNoSupport: Boolean? = null, // var NoLogsNoSupport: Boolean? = null, // Currently unused
var Machine: String? = null, // var Machine: String? = null, // Currently unused
var RoutableIPs: List<Prefix>? = null, // var RoutableIPs: List<Prefix>? = null, // Currently unused
var Services: List<Service>? = null, // var Services: List<Service>? = null, // Currently unused
var Location: Location? = null, var Location: Location? = null,
) )
@Serializable @Serializable
data class Node( data class Node(
var ID: NodeID, // var ID: NodeID, // Currently unused
var StableID: StableNodeID, var StableID: StableNodeID,
var Name: String, var Name: String,
var User: UserID, var User: UserID,
var Sharer: UserID? = null, // var Sharer: UserID? = null, // Currently unused
var Key: KeyNodePublic, // var Key: KeyNodePublic, // Currently unused
var KeyExpiry: String, var KeyExpiry: String,
var Machine: MachineKey, // var Machine: MachineKey, // Currently unused
var Addresses: List<Prefix>? = null, var Addresses: List<Prefix>? = null,
var AllowedIPs: List<Prefix>? = null, var AllowedIPs: List<Prefix>? = null,
var Endpoints: List<String>? = null, // var Endpoints: List<String>? = null, // Currently unused
var Hostinfo: Hostinfo, var Hostinfo: Hostinfo,
var Created: Time, // var Created: Time, // Currently unused
var LastSeen: Time? = null, // var LastSeen: Time? = null, // Currently unused
var Online: Boolean? = null, var Online: Boolean? = null,
var Capabilities: List<String>? = null, var Capabilities: List<String>? = null,
var ComputedName: String, var ComputedName: String,
var ComputedNameWithHost: String // var ComputedNameWithHost: String // Currently unused
) { ) {
val isAdmin: Boolean val isAdmin: Boolean
get() = (Capabilities ?: emptyList()).contains("https://tailscale.com/cap/is-admin") get() = (Capabilities ?: emptyList()).contains("https://tailscale.com/cap/is-admin")
// isExitNode reproduces the Go logic in local.go peerStatusFromNode
val isExitNode: Boolean =
AllowedIPs?.contains("0.0.0.0/0") ?: false && AllowedIPs?.contains("::/0") ?: false
} }
@Serializable // Currently unused
data class Service(var Proto: String, var Port: Int, var Description: String? = null) // @Serializable
// data class Service(var Proto: String, var Port: Int, var Description: String? = null)
@Serializable @Serializable
data class NetworkProfile(var MagicDNSName: String? = null, var DomainName: String? = null) data class NetworkProfile(var MagicDNSName: String? = null, var DomainName: String? = null)
@Serializable @Serializable
data class Location( data class Location(
var Country: String? = null, var Country: String? = null,
var CountryCode: String? = null, var CountryCode: String? = null,
var City: String? = null, var City: String? = null,
var CityCode: String? = null, // var CityCode: String? = null, // Currently unused
var Priority: Int? = null var Priority: Int? = null
) )
@Serializable // Currently unused
data class DNSConfig( // @Serializable
var Resolvers: List<DnsType.Resolver>? = null, // data class DNSConfig(
var Routes: Map<String, List<DnsType.Resolver>?>? = null, // var Resolvers: List<DnsType.Resolver>? = null,
var FallbackResolvers: List<DnsType.Resolver>? = null, // var Routes: Map<String, List<DnsType.Resolver>?>? = null,
var Domains: List<String>? = null, // var FallbackResolvers: List<DnsType.Resolver>? = null,
var Nameservers: List<Addr>? = null // var Domains: List<String>? = null,
) // var Nameservers: List<Addr>? = null
// )
} }

@ -7,11 +7,11 @@ import kotlinx.serialization.Serializable
typealias Addr = String typealias Addr = String
typealias Prefix = String typealias Prefix = String
typealias NodeID = Long //typealias NodeID = Long // Currently unused
typealias KeyNodePublic = String //typealias KeyNodePublic = String // Currently unused
typealias MachineKey = String //typealias MachineKey = String // Currently unused
typealias UserID = Long typealias UserID = Long
typealias Time = String //typealias Time = String // Currently unused
typealias StableNodeID = String typealias StableNodeID = String
typealias BugReportID = String typealias BugReportID = String

@ -6,7 +6,7 @@ package com.tailscale.ipn.ui.notifier
import android.util.Log import android.util.Log
import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.Ipn.Notify import com.tailscale.ipn.ui.model.Ipn.Notify
import com.tailscale.ipn.ui.model.Netmap import com.tailscale.ipn.ui.model.NetworkMap
import com.tailscale.ipn.ui.util.set import com.tailscale.ipn.ui.util.set
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -32,7 +32,7 @@ object Notifier {
private val isReady = CompletableDeferred<Boolean>() private val isReady = CompletableDeferred<Boolean>()
val state: StateFlow<Ipn.State> = MutableStateFlow(Ipn.State.NoState) val state: StateFlow<Ipn.State> = MutableStateFlow(Ipn.State.NoState)
val netmap: StateFlow<Netmap.NetworkMap?> = MutableStateFlow(null) val netmap: StateFlow<NetworkMap?> = MutableStateFlow(null)
val prefs: StateFlow<Ipn.Prefs?> = MutableStateFlow(null) val prefs: StateFlow<Ipn.Prefs?> = MutableStateFlow(null)
val engineStatus: StateFlow<Ipn.EngineStatus?> = MutableStateFlow(null) val engineStatus: StateFlow<Ipn.EngineStatus?> = MutableStateFlow(null)
val tailFSShares: StateFlow<Map<String, String>?> = MutableStateFlow(null) val tailFSShares: StateFlow<Map<String, String>?> = MutableStateFlow(null)

@ -4,7 +4,7 @@
package com.tailscale.ipn.ui.util package com.tailscale.ipn.ui.util
import com.tailscale.ipn.ui.model.Netmap import com.tailscale.ipn.ui.model.NetworkMap
import com.tailscale.ipn.ui.model.Tailcfg import com.tailscale.ipn.ui.model.Tailcfg
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
@ -37,7 +37,7 @@ class PeerCategorizer(scope: CoroutineScope) {
} }
} }
private fun regenerateGroupedPeers(netmap: Netmap.NetworkMap): List<PeerSet> { private fun regenerateGroupedPeers(netmap: NetworkMap): List<PeerSet> {
val peers: List<Tailcfg.Node> = netmap.Peers ?: return emptyList() val peers: List<Tailcfg.Node> = netmap.Peers ?: return emptyList()
val selfNode = netmap.SelfNode val selfNode = netmap.SelfNode
var grouped = mutableMapOf<UserID, MutableList<Tailcfg.Node>>() var grouped = mutableMapOf<UserID, MutableList<Tailcfg.Node>>()

@ -4,17 +4,20 @@
package com.tailscale.ipn.ui.viewModel package com.tailscale.ipn.ui.viewModel
import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.tailscale.ipn.ui.localapi.Client import com.tailscale.ipn.ui.localapi.Client
import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.StableNodeID import com.tailscale.ipn.ui.model.StableNodeID
import com.tailscale.ipn.ui.notifier.Notifier
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
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.util.TreeMap import java.util.TreeMap
data class ExitNodePickerNav( data class ExitNodePickerNav(
@ -52,66 +55,66 @@ class ExitNodePickerViewModel(private val nav: ExitNodePickerNav) : IpnViewModel
val anyActive: StateFlow<Boolean> = MutableStateFlow(false) val anyActive: StateFlow<Boolean> = MutableStateFlow(false)
init { init {
Client(viewModelScope).status { result -> viewModelScope.launch {
result.onFailure { Notifier.netmap.combine(Notifier.prefs) { netmap, prefs -> Pair(netmap, prefs) }
Log.e(TAG, "getStatus: ${it.message}") .stateIn(viewModelScope).collect { (netmap, prefs) ->
}.onSuccess { val exitNodeId = prefs?.ExitNodeID
it.Peer?.values?.let { peers -> netmap?.Peers?.let { peers ->
val allNodes = peers.filter { it.ExitNodeOption }.map { val allNodes = peers.filter { it.isExitNode }.map {
ExitNode( ExitNode(
id = it.ID, id = it.StableID,
label = it.DNSName, label = it.Name,
online = it.Online, online = it.Online ?: false,
selected = it.ExitNode, selected = it.StableID == exitNodeId,
mullvad = it.DNSName.endsWith(".mullvad.ts.net."), mullvad = it.Name.endsWith(".mullvad.ts.net."),
priority = it.Location?.Priority ?: 0, priority = it.Hostinfo?.Location?.Priority ?: 0,
countryCode = it.Location?.CountryCode ?: "", countryCode = it.Hostinfo?.Location?.CountryCode ?: "",
country = it.Location?.Country ?: "", country = it.Hostinfo?.Location?.Country ?: "",
city = it.Location?.City ?: "", city = it.Hostinfo?.Location?.City ?: "",
) )
} }
val tailnetNodes = allNodes.filter { !it.mullvad } val tailnetNodes = allNodes.filter { !it.mullvad }
tailnetExitNodes.set(tailnetNodes.sortedWith { a, b -> tailnetExitNodes.set(tailnetNodes.sortedWith { a, b ->
a.label.compareTo( a.label.compareTo(
b.label b.label
) )
}) })
val mullvadExitNodes = allNodes.filter { val mullvadExitNodes = allNodes.filter {
// Pick all mullvad nodes that are online or the currently selected // Pick all mullvad nodes that are online or the currently selected
it.mullvad && (it.selected || it.online) it.mullvad && (it.selected || it.online)
}.groupBy { }.groupBy {
// Group by countryCode // Group by countryCode
it.countryCode it.countryCode
}.mapValues { (_, nodes) ->
// Group by city
nodes.groupBy {
it.city
}.mapValues { (_, nodes) -> }.mapValues { (_, nodes) ->
// Pick one node per city, either the selected one or the best // Group by city
// available nodes.groupBy {
nodes.sortedWith { a, b -> it.city
if (a.selected && !b.selected) { }.mapValues { (_, nodes) ->
-1 // Pick one node per city, either the selected one or the best
} else if (b.selected && !a.selected) { // available
1 nodes.sortedWith { a, b ->
} else { if (a.selected && !b.selected) {
b.priority.compareTo(a.priority) -1
} } else if (b.selected && !a.selected) {
}.first() 1
}.values.sortedBy { it.city.lowercase() } } else {
} b.priority.compareTo(a.priority)
mullvadExitNodesByCountryCode.set(mullvadExitNodes) }
}.first()
}.values.sortedBy { it.city.lowercase() }
}
mullvadExitNodesByCountryCode.set(mullvadExitNodes)
val bestAvailableByCountry = mullvadExitNodes.mapValues { (_, nodes) -> val bestAvailableByCountry = mullvadExitNodes.mapValues { (_, nodes) ->
nodes.minByOrNull { -1 * it.priority }!! nodes.minByOrNull { -1 * it.priority }!!
} }
mullvadBestAvailableByCountry.set(bestAvailableByCountry) mullvadBestAvailableByCountry.set(bestAvailableByCountry)
anyActive.set(allNodes.any { it.selected }) anyActive.set(allNodes.any { it.selected })
}
} }
}
} }
} }

Loading…
Cancel
Save