// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package com.tailscale.ipn.ui.model import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.tailscale.ipn.R import com.tailscale.ipn.ui.Links import com.tailscale.ipn.ui.theme.off import com.tailscale.ipn.ui.theme.on import com.tailscale.ipn.ui.util.ComposableStringFormatter import com.tailscale.ipn.ui.util.DisplayAddress import com.tailscale.ipn.ui.util.TimeUtil import com.tailscale.ipn.ui.viewModel.PeerSettingInfo import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement import java.util.Date class Tailcfg { @Serializable data class ClientVersion( var RunningLatest: Boolean? = null, var LatestVersion: String? = null, var UrgentSecurityUpdate: Boolean? = null, var Notify: Boolean? = null, var NotifyURL: String? = null, var NotifyText: String? = null ) @Serializable data class UserProfile( val ID: Long, val DisplayName: String, val LoginName: String, val ProfilePicURL: String? = null, ) { fun isTaggedDevice(): Boolean { return LoginName == "tagged-devices" } } @Serializable data class Hostinfo( var IPNVersion: String? = null, var FrontendLogID: String? = null, var BackendLogID: String? = null, var OS: String? = null, var OSVersion: String? = null, var Env: String? = null, var Distro: String? = null, var DistroVersion: String? = null, var DistroCodeName: String? = null, var Desktop: Boolean? = null, var Package: String? = null, var DeviceModel: String? = null, var ShareeNode: Boolean? = null, var Hostname: String? = null, var ShieldsUp: Boolean? = null, var NoLogsNoSupport: Boolean? = null, var Machine: String? = null, var RoutableIPs: List? = null, var Services: List? = null, var Location: Location? = null, ) @Serializable data class Node( var ID: NodeID, var StableID: StableNodeID, var Name: String, var User: UserID, var Sharer: UserID? = null, var Key: KeyNodePublic, var KeyExpiry: String, var Machine: MachineKey, var Addresses: List? = null, var AllowedIPs: List? = null, var Endpoints: List? = null, var Hostinfo: Hostinfo, var Created: Time, var LastSeen: Time? = null, var Online: Boolean? = null, var Capabilities: List? = null, var CapMap: Map? = null, var ComputedName: String?, var ComputedNameWithHost: String? ) { val isAdmin: Boolean get() = Capabilities?.contains("https://tailscale.com/cap/is-admin") == true || CapMap?.contains("https://tailscale.com/cap/is-admin") == true // Derives the url to directly administer a node val nodeAdminUrl: String get() = primaryIPv4Address?.let { "${Links.ADMIN_URL}/machines/${it}" } ?: Links.ADMIN_URL val primaryIPv4Address: String? get() = displayAddresses.firstOrNull { it.type == DisplayAddress.addrType.V4 }?.address val primaryIPv6Address: String? get() = displayAddresses.firstOrNull { it.type == DisplayAddress.addrType.V6 }?.address // isExitNode reproduces the Go logic in local.go peerStatusFromNode val isExitNode: Boolean = AllowedIPs?.contains("0.0.0.0/0") ?: false && AllowedIPs?.contains("::/0") ?: false val isMullvadNode: Boolean get() = Name.endsWith(".mullvad.ts.net.") val displayName: String get() = ComputedName ?: Name fun isSelfNode(netmap: Netmap.NetworkMap): Boolean = StableID == netmap.SelfNode.StableID fun connectedOrSelfNode(nm: Netmap.NetworkMap?) = Online == true || StableID == nm?.SelfNode?.StableID fun connectedStrRes(nm: Netmap.NetworkMap?) = if (connectedOrSelfNode(nm)) R.string.connected else R.string.not_connected @Composable fun connectedColor(nm: Netmap.NetworkMap?) = if (connectedOrSelfNode(nm)) MaterialTheme.colorScheme.on else MaterialTheme.colorScheme.off val nameWithoutTrailingDot = Name.trimEnd('.') val displayAddresses: List get() { var addresses = mutableListOf() addresses.add(DisplayAddress(nameWithoutTrailingDot)) Addresses?.let { addresses.addAll(it.map { addr -> DisplayAddress(addr) }) } return addresses } val info: List get() { val result = mutableListOf() if (Hostinfo.OS?.isNotEmpty() == true) { result.add( PeerSettingInfo(R.string.os, ComposableStringFormatter(Hostinfo.OS!!)), ) } result.add(PeerSettingInfo(R.string.key_expiry, TimeUtil.keyExpiryFromGoTime(KeyExpiry))) return result } @Composable fun expiryLabel(): String { if (KeyExpiry == GoZeroTimeString) { return stringResource(R.string.deviceKeyNeverExpires) } val expDate = TimeUtil.dateFromGoString(KeyExpiry) val template = if (expDate > Date()) R.string.deviceKeyExpires else R.string.deviceKeyExpired return stringResource(template, TimeUtil.keyExpiryFromGoTime(KeyExpiry).getString()) } } @Serializable data class Service(var Proto: String, var Port: Int, var Description: String? = null) @Serializable data class NetworkProfile(var MagicDNSName: String? = null, var DomainName: String? = null) @Serializable data class Location( var Country: String? = null, var CountryCode: String? = null, var City: String? = null, var CityCode: String? = null, var Priority: Int? = null ) @Serializable data class DNSConfig( var Resolvers: List? = null, var Routes: Map?>? = null, var FallbackResolvers: List? = null, var Domains: List? = null, var Nameservers: List? = null ) }