android: don't list the self node twice in the peer list - #846
Open
hrueger wants to merge 1 commit into
Open
Conversation
PeerCategorizer.regenerateGroupedPeers appends netmap.SelfNode to netmap.Peers unconditionally. When the control server includes a node in its own peer list, that node is then listed twice. MainView keys its LazyColumn by Tailcfg.Node.StableID, so Compose rejects the duplicate key with java.lang.IllegalArgumentException: Key "20" was already used. If you are using LazyColumn/Row please make sure you provide a unique key for each item. thrown on the main thread from LayoutNodeSubcompositionsState.subcompose during measure. The process dies a few seconds after launch, on every launch, once the netmap arrives. Reproduced against a Headscale control server, which assigns integer StableNodeIDs. The netmap delivered right after login is clean and a subsequent one includes the node itself: selfStableID=20 peerCount=6 selfInPeers=0 peerIDs=[6, 7, 8, 13, 15, 19] selfStableID=20 peerCount=7 selfInPeers=1 peerIDs=[6, 7, 8, 13, 15, 19, 20] The duplicated key tracked the device's own node ID across three re-registrations (18, 19, 20), confirming the duplicate is always the self node rather than a peer collision. Drop the self node from the peer list before appending the authoritative SelfNode, so the client does not take a fatal exception on data a control server is free to send. Signed-off-by: Hannes Rüger <hannes@rueger.events> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hi folks,
I recently started hitting a bug where my app would crash immediately after connecting. With the help of Claude Code, I debugged this and this fix is working for me with my Headscale / Headplane setup. You dont need to merge this directly, just take it as an example fix for the bug. Thanks in advance! 🙏
Claude follows below:
The bug
The app crashes a few seconds after launch, on every launch, and is completely unusable — while the VPN itself keeps working, because
IPNServicerestarts after each crash.PeerCategorizer.regenerateGroupedPeersappendsnetmap.SelfNodetonetmap.Peersunconditionally:When the control server includes a node in that node's own peer list, the node is listed twice.
MainViewkeys itsLazyColumnbyTailcfg.Node.StableID(MainView.kt:647), so Compose rejects the duplicate key with a fatal exception during measure. There are no app frames in the stack — the throw happens insidesubcompose— which makes this awkward to diagnose from a crash report alone.Evidence
Observed against a Headscale control server. Headscale assigns integer
StableNodeIDs, which is what makes the duplicated key legible as a node ID; the underlying problem is not specific to Headscale.A temporary log in
regenerateGroupedPeersshows the netmap delivered right after login is clean, and a subsequent one contains the node itself:That second update is what kills the app, and it explains the delay between launch and crash.
The duplicated key tracked the device's own node ID across three separate re-registrations —
"18", then"19", then"20"— confirming the duplicate is always the self node rather than a collision between two peers.The fix
Drop the self node from the peer list before appending the authoritative
SelfNode. The client shouldn't take a fatal exception on data a control server is free to send.Testing
PeerCategorizerTestcovers both directions: self present inPeersmust be listed exactly once, and the ordinary case where self is absent must still list it. The first test fails on the unfixed code at the uniqueness assertion while the control case passes, so it's a genuine regression test../gradlew testDebugUnitTestpasses; the new test fails without the fix../gradlew ktfmtFormatreformatted nothing.main, Samsung SM-G781B, Android 13 (API 33). Before the fix, the app crashed within ~3s of every launch. After, it stays up, processes both netmaps including the one that previously killed it, and renders the device list normally.First hit on 1.102.3 from the Play Store; reproduced and fixed against
mainat 26e4361 (1.103.159).Disclosure
This contribution was produced entirely with Claude Code (Opus 5) — the investigation, the root-cause analysis, the fix, the test, and this description. A human directed the work and approved each step, but did not write the code.
Reviewers may want to weigh that accordingly. The claims above are reproducible rather than asserted: the diagnostic output, the three node IDs, and the before/after device behaviour were all captured over adb against a live tailnet, and the test fails without the one-line change.
🤖 Generated with Claude Code