@@ -8,7 +8,10 @@ import com.tailscale.ipn.ui.model.Empty
88import com.tailscale.ipn.ui.model.Health
99import com.tailscale.ipn.ui.model.Ipn
1010import com.tailscale.ipn.ui.model.Ipn.Notify
11+ import com.tailscale.ipn.ui.model.IpnState
1112import com.tailscale.ipn.ui.model.Netmap
13+ import com.tailscale.ipn.ui.model.NodeID
14+ import com.tailscale.ipn.ui.model.Tailcfg
1215import com.tailscale.ipn.ui.util.set
1316import com.tailscale.ipn.util.TSLog
1417import 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}
0 commit comments