Skip to content

Commit 752d39e

Browse files
committed
android: stop watching legacy netmap notifications
Follow up to tailscale/tailscale#19607, which introduced the constant-time node add/remove path and temporarily kept Android on legacy Notify.NetMap emission. Move the Android app notification watcher to InitialStatus plus peer-change notifications, and opt both Android watchers out of runtime NetMap delivery. Updates tailscale/tailscale#12542 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
1 parent 8fb1ef9 commit 752d39e

4 files changed

Lines changed: 116 additions & 20 deletions

File tree

android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ class Ipn {
4242
val OutgoingFiles: List<OutgoingFile>? = null,
4343
val State: Int? = null,
4444
var Prefs: Prefs? = null,
45-
var NetMap: Netmap.NetworkMap? = null,
45+
var SelfChange: Tailcfg.Node? = null,
46+
var InitialStatus: IpnState.Status? = null,
47+
var PeersChanged: List<Tailcfg.Node>? = null,
48+
var PeersRemoved: List<NodeID>? = null,
49+
var UserProfiles: Map<String, Tailcfg.UserProfile>? = null,
4650
var Engine: EngineStatus? = null,
4751
var BrowseToURL: String? = null,
4852
var BackendLogId: String? = null,

android/src/main/java/com/tailscale/ipn/ui/model/IpnState.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,33 @@ class IpnState {
1818
@Serializable
1919
data class PeerStatus(
2020
val ID: StableNodeID,
21+
val NodeID: NodeID = 0,
22+
val PublicKey: KeyNodePublic = "",
2123
val HostName: String,
2224
val DNSName: String,
25+
val OS: String = "",
26+
val UserID: UserID = 0,
27+
val AltSharerUserID: UserID = 0,
2328
val TailscaleIPs: List<Addr>? = null,
29+
val AllowedIPs: List<Prefix>? = null,
2430
val Tags: List<String>? = null,
2531
val PrimaryRoutes: List<String>? = null,
2632
val Addrs: List<String>? = null,
2733
val CurAddr: String? = null,
2834
val Relay: String? = null,
2935
val PeerRelay: String? = null,
36+
val LastSeen: String? = null,
3037
val Online: Boolean,
3138
val ExitNode: Boolean,
3239
val ExitNodeOption: Boolean,
3340
val Active: Boolean,
3441
val PeerAPIURL: List<String>? = null,
3542
val Capabilities: List<String>? = null,
43+
val CapMap: Map<String, kotlinx.serialization.json.JsonElement?>? = null,
3644
val SSH_HostKeys: List<String>? = null,
3745
val ShareeNode: Boolean? = null,
3846
val Expired: Boolean? = null,
47+
val KeyExpiry: String? = null,
3948
val Location: Tailcfg.Location? = null,
4049
) {
4150
fun computedName(status: Status): String {

android/src/main/java/com/tailscale/ipn/ui/notifier/Notifier.kt

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import com.tailscale.ipn.ui.model.Empty
88
import com.tailscale.ipn.ui.model.Health
99
import com.tailscale.ipn.ui.model.Ipn
1010
import com.tailscale.ipn.ui.model.Ipn.Notify
11+
import com.tailscale.ipn.ui.model.IpnState
1112
import com.tailscale.ipn.ui.model.Netmap
13+
import com.tailscale.ipn.ui.model.NodeID
14+
import com.tailscale.ipn.ui.model.Tailcfg
1215
import com.tailscale.ipn.ui.util.set
1316
import com.tailscale.ipn.util.TSLog
1417
import kotlinx.coroutines.CoroutineScope
@@ -38,7 +41,8 @@ object Notifier {
3841
// General IPN Bus State
3942
private val _state = MutableStateFlow(Ipn.State.NoState)
4043
val state: StateFlow<Ipn.State> = _state
41-
val netmap: StateFlow<Netmap.NetworkMap?> = MutableStateFlow(null)
44+
private val _netmap = MutableStateFlow<Netmap.NetworkMap?>(null)
45+
val netmap: StateFlow<Netmap.NetworkMap?> = _netmap
4246
val prefs: StateFlow<Ipn.Prefs?> = MutableStateFlow(null)
4347
val engineStatus: StateFlow<Ipn.EngineStatus?> = MutableStateFlow(null)
4448
val tailFSShares: StateFlow<Map<String, String>?> = MutableStateFlow(null)
@@ -70,16 +74,18 @@ object Notifier {
7074
}
7175
scope.launch(Dispatchers.IO) {
7276
val mask =
73-
NotifyWatchOpt.Netmap.value or
74-
NotifyWatchOpt.Prefs.value or
77+
NotifyWatchOpt.Prefs.value or
7578
NotifyWatchOpt.InitialState.value or
79+
NotifyWatchOpt.PeerChanges.value or
80+
NotifyWatchOpt.NoNetmap.value or
81+
NotifyWatchOpt.InitialStatus.value or
7682
NotifyWatchOpt.InitialHealthState.value or
7783
NotifyWatchOpt.RateLimitNetmaps.value
7884
manager =
7985
app.watchNotifications(mask.toLong()) { notification ->
8086
val notify = decoder.decodeFromStream<Notify>(notification.inputStream())
8187
notify.State?.let { state.set(Ipn.State.fromInt(it)) }
82-
notify.NetMap?.let(netmap::set)
88+
updateNetworkMap(notify)
8389
notify.Prefs?.let(prefs::set)
8490
notify.Engine?.let(engineStatus::set)
8591
notify.TailFSShares?.let(tailFSShares::set)
@@ -114,15 +120,103 @@ object Notifier {
114120
EngineUpdates(1),
115121
InitialState(2),
116122
Prefs(4),
117-
Netmap(8),
118123
NoPrivateKey(16),
119124
InitialTailFSShares(32),
120125
InitialOutgoingFiles(64),
121126
InitialHealthState(128),
122127
RateLimitNetmaps(256),
128+
PeerChanges(4096),
129+
NoNetmap(8192),
130+
InitialStatus(16384),
123131
}
124132

125133
fun setState(newState: Ipn.State) {
126134
_state.value = newState
127135
}
136+
137+
private fun updateNetworkMap(notify: Notify) {
138+
notify.InitialStatus?.let { _netmap.set(it.toNetworkMapOrNull()) }
139+
140+
val cur = _netmap.value ?: return
141+
142+
var next: Netmap.NetworkMap = cur
143+
val profiles = notify.UserProfiles
144+
if (profiles != null) {
145+
next = next.copy(UserProfiles = next.UserProfiles + profiles)
146+
}
147+
val self = notify.SelfChange
148+
if (self != null) {
149+
next = next.copy(SelfNode = self, AllCaps = self.capabilities())
150+
}
151+
val removed = notify.PeersRemoved
152+
if (!removed.isNullOrEmpty()) {
153+
val removedIDs = removed.toSet()
154+
next = next.copy(Peers = next.Peers?.filterNot { removedIDs.contains(it.ID) })
155+
}
156+
val changed = notify.PeersChanged
157+
if (!changed.isNullOrEmpty()) {
158+
next = next.copy(Peers = mergePeers(next.Peers, changed))
159+
}
160+
if (next != cur) {
161+
_netmap.set(next)
162+
}
163+
}
164+
165+
private fun mergePeers(
166+
peers: List<Tailcfg.Node>?,
167+
changed: List<Tailcfg.Node>
168+
): List<Tailcfg.Node> {
169+
val byID = linkedMapOf<NodeID, Tailcfg.Node>()
170+
peers.orEmpty().forEach { byID[it.ID] = it }
171+
changed.forEach { byID[it.ID] = it }
172+
return byID.values.toList()
173+
}
174+
175+
private fun IpnState.Status.toNetworkMapOrNull(): Netmap.NetworkMap? {
176+
val users = User?.mapKeys { it.key.toString() }.orEmpty()
177+
val self = Self?.toNode(this) ?: return null
178+
return Netmap.NetworkMap(
179+
SelfNode = self,
180+
Peers = Peer?.values?.map { it.toNode(this) }.orEmpty(),
181+
Domain = CurrentTailnet?.Name.orEmpty(),
182+
UserProfiles = users,
183+
TKAEnabled = false,
184+
DNS = null,
185+
AllCaps = self.capabilities())
186+
}
187+
188+
private fun Tailcfg.Node.capabilities() = CapMap?.keys?.toList() ?: Capabilities.orEmpty()
189+
190+
private fun IpnState.PeerStatus.toNode(status: IpnState.Status): Tailcfg.Node {
191+
val computedName = computedName(status)
192+
val allowedIPs =
193+
AllowedIPs
194+
?: buildList {
195+
TailscaleIPs?.let { addAll(it) }
196+
PrimaryRoutes?.let { addAll(it) }
197+
if (ExitNodeOption) {
198+
add("0.0.0.0/0")
199+
add("::/0")
200+
}
201+
}
202+
return Tailcfg.Node(
203+
ID = NodeID,
204+
StableID = ID,
205+
Name = DNSName.ifEmpty { HostName },
206+
User = UserID,
207+
Sharer = AltSharerUserID.takeIf { it != 0L },
208+
Key = PublicKey,
209+
KeyExpiry = KeyExpiry,
210+
Addresses = TailscaleIPs,
211+
AllowedIPs = allowedIPs,
212+
Hostinfo =
213+
Tailcfg.Hostinfo(
214+
OS = OS, Hostname = HostName, ShareeNode = ShareeNode, Location = Location),
215+
LastSeen = LastSeen,
216+
Online = Online,
217+
Capabilities = Capabilities,
218+
CapMap = CapMap,
219+
ComputedName = computedName,
220+
ComputedNameWithHost = computedName)
221+
}
128222
}

libtailscale/backend.go

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import (
3434
"tailscale.com/tsd"
3535
"tailscale.com/types/logger"
3636
"tailscale.com/types/logid"
37-
"tailscale.com/types/netmap"
3837
"tailscale.com/util/eventbus"
3938
"tailscale.com/wgengine"
4039
"tailscale.com/wgengine/netstack"
@@ -177,20 +176,15 @@ func (a *App) runBackend(ctx context.Context, hardwareAttestation bool) error {
177176
b.avoidEmptyDNS = a.isChromeOS()
178177

179178
var (
180-
cfg configPair
181-
state ipn.State
182-
networkMap *netmap.NetworkMap
179+
cfg configPair
180+
state ipn.State
183181
)
184182

185183
stateCh := make(chan ipn.State)
186-
netmapCh := make(chan *netmap.NetworkMap)
187-
go b.backend.WatchNotifications(ctx, ipn.NotifyInitialNetMap|ipn.NotifyInitialPrefs|ipn.NotifyInitialState, func() {}, func(notify *ipn.Notify) bool {
184+
go b.backend.WatchNotifications(ctx, ipn.NotifyInitialPrefs|ipn.NotifyInitialState|ipn.NotifyNoNetMap, func() {}, func(notify *ipn.Notify) bool {
188185
if notify.State != nil {
189186
stateCh <- *notify.State
190187
}
191-
if notify.NetMap != nil {
192-
netmapCh <- notify.NetMap
193-
}
194188
return true
195189
})
196190
for {
@@ -206,8 +200,6 @@ func (a *App) runBackend(ctx context.Context, hardwareAttestation bool) error {
206200
a.closeVpnService(err, b)
207201
}
208202
}
209-
case n := <-netmapCh:
210-
networkMap = n
211203
case c := <-configs:
212204
cfg = c
213205
if vpnService.service == nil || !b.isConfigNonNilAndDifferent(cfg.rcfg, cfg.dcfg) {
@@ -253,9 +245,6 @@ func (a *App) runBackend(ctx context.Context, hardwareAttestation bool) error {
253245

254246
vpnService.service = s
255247

256-
if networkMap != nil {
257-
// TODO
258-
}
259248
if state >= ipn.Starting && b.isConfigNonNilAndDifferent(cfg.rcfg, cfg.dcfg) {
260249
if err := b.updateTUN(cfg.rcfg, cfg.dcfg); err != nil {
261250
a.closeVpnService(err, b)

0 commit comments

Comments
 (0)