feat: add block & blackhole peer protection#601
Conversation
Greptile SummaryThis PR implements a two-layer peer protection system: LXMF-level blocking (via Issues found:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant UI as UI (ChatsScreen / MessagingScreen / AnnounceDetailScreen)
participant VM as ViewModel (ChatsVM / MessagingVM / AnnounceStreamVM)
participant Repo as BlockedPeerRepository
participant DB as Room DB (BlockedPeerDao)
participant Binder as ReticulumServiceBinder
participant Py as BlockingManager (Python)
participant LXMF as LXMRouter
participant RNS as RNS.Reticulum (Transport)
UI->>VM: blockUser(peerHash, identityHash, blackhole, deleteConv)
VM->>Repo: blockPeer(peerHash, identityHash, displayName, blackhole)
Repo->>DB: insertBlockedPeer(entity)
VM->>Binder: reticulumProtocol.blockDestination(peerHash)
Binder->>Py: wrapper.block_destination(hex)
Py->>LXMF: router.ignore_destination(bytes)
opt blackhole && identityHash != null
VM->>Binder: reticulumProtocol.blackholeIdentity(identityHash)
Binder->>Py: wrapper.blackhole_identity(hex)
Py->>RNS: reticulum.blackhole_identity(bytes)
end
opt deleteConversation
VM->>Repo: conversationRepository.deleteConversation(peerHash)
end
Note over DB,LXMF: On app restart — LXMF ignore list must be restored
Binder->>DB: blockedPeerDao.getBlockedPeerHashes(activeIdentityHash)
Binder->>Py: wrapper.restore_blocked_destinations(pyList)
Py->>LXMF: router.ignore_destination(bytes) per hash
Note over DB: Defense-in-depth: ServicePersistenceManager<br/>checks DB before accepting any message
|
app/src/main/java/com/lxmf/messenger/service/binder/ReticulumServiceBinder.kt
Show resolved
Hide resolved
app/src/main/java/com/lxmf/messenger/service/binder/ReticulumServiceBinder.kt
Show resolved
Hide resolved
app/src/main/java/com/lxmf/messenger/service/binder/ReticulumServiceBinder.kt
Show resolved
Hide resolved
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
13e99f6 to
f70c89a
Compare
| blockedPeerRepository.unblockPeer(peer.peerHash) | ||
| reticulumProtocol.unblockDestination(peer.peerHash) |
There was a problem hiding this comment.
Bug: The unblockUser function updates the database before the transport layer. If the transport layer call fails, the user remains blocked but appears unblocked in the UI.
Severity: MEDIUM
Suggested Fix
Reverse the order of operations. First, call reticulumProtocol.unblockDestination to update the transport layer. Only upon its success should the database be updated by calling blockedPeerRepository.unblockPeer. Alternatively, implement a rollback mechanism for the database change if the transport layer call fails.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location:
app/src/main/java/com/lxmf/messenger/viewmodel/BlockedUsersViewModel.kt#L48-L49
Potential issue: In the `unblockUser` function, a user is removed from the local
database via `blockedPeerRepository.unblockPeer` before the corresponding call to
`reticulumProtocol.unblockDestination` removes them from the transport layer's ignore
list. There is no error handling to roll back the database change if the transport layer
call fails. This results in an inconsistent state where the user appears unblocked in
the application's UI, but their messages will continue to be silently dropped by the
transport layer.
app/src/main/java/com/lxmf/messenger/service/binder/ReticulumServiceBinder.kt
Show resolved
Hide resolved
Two-layer defense against spam and harassment on Reticulum mesh networks: - Block (LXMF-level): silently drops messages from a peer via LXMRouter.ignore_destination() - Blackhole (Reticulum Transport-level): drops path entries and invalidates announces via reticulum.blackhole_identity() Database: new blocked_peers table with composite PK, Room migration 40→41, ConversationDao filters blocked peers from chat list. Service: defense-in-depth DB check in ServicePersistenceManager, LXMF block list restore at startup. UI: BlockUserDialog with optional delete + blackhole checkboxes, entry points from chat context menu, messaging overflow menu, and announce detail screen. BlockedUsersScreen in Settings for management. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Suppress TooManyFunctions on DatabaseModule (Hilt modules legitimately have one @provides per DAO) - Remove unused deleteConversation param from AnnounceStreamViewModel.blockPeer() - Add real assertions (slot captures + assertEquals) to ChatsViewModelTest block tests to satisfy NoVerifyOnlyTests rule - Replace string interpolation with JSONObject in ReticulumServiceBinder error JSON (prevents invalid JSON from exception messages with special chars) - Add interactive blackhole Switch toggle to BlockedPeerCard in BlockedUsersScreen Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add isTransportEnabled stub to AnnounceDetailScreenTest mock (fixes Shard 3/4 failure) - Add test_blocking_manager.py with 25 tests covering all BlockingManager methods - Fix fragile success check: treat None return as failure (not just False) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use `result is True` instead of `result is not None and result is not False` for blackhole/unblackhole success checks (prevents None being treated as success) - Make restore_blocked_destinations handle per-hash errors gracefully instead of failing atomically (a single bad hash no longer aborts restoring all others) - Add test_restore_partial_failure test case Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…wModelTest The constructor call at line 992 was missing the blockedPeerRepository argument added in the block & blackhole feature, causing CI compilation failure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
caf01f7 to
89edff0
Compare
Summary
LXMRouter.ignore_destination(). Operates on destination hashes. Blocked conversations are hidden from the chat list.reticulum.blackhole_identity(). Operates on identity hashes. Always available as a toggle — when transport is disabled, a warning explains the blackhole will activate when transport is later enabled.ServicePersistenceManagercatches messages during the window between Python init and LXMF ignore list restore.Changes
Database (Layer 1)
BlockedPeerEntitywith composite PK (peerHash+identityHash), FK cascade toLocalIdentityEntityBlockedPeerDaowith suspend + Flow methods (CRUD, hash lists, blackhole queries, peer count)blocked_peerstableConversationDao:NOT EXISTSfilter hides blocked peers from enriched conversation queriesBlockedPeerRepositorywith active-identity-scoped methodsPython (Layer 2)
blocking_manager.py:BlockingManagerclass with block/unblock/blackhole/restore methodsreticulum_wrapper.py: thin delegation toBlockingManagerService (Layer 3)
ServicePersistenceManager:isBlockedPeer()DB check beforeshouldBlockUnknownSendercheckReticulumServiceBinder: 6 new AIDL methods, LXMF block list restore at startupReticulumProtocol+ServiceReticulumProtocol: block/unblock/blackhole bridge methodsUI (Layer 4)
BlockUserDialog: confirmation dialog with "delete messages" and "blackhole" checkboxesBlockedUsersScreen: Settings management screen with unblock + blackhole toggle per peerPrivacyCard: "Blocked Users (N)" navigation row in SettingsTest plan
BlockedPeerDaoTest— 14 tests (insert, query, delete, FK cascade, Flow reactivity, hash lists, blackhole toggle, upsert)ServicePersistenceManagerDatabaseTest— 2 new tests (blocks message from blocked peer, allows message from non-blocked peer)ChatsViewModelTest— 5 new tests (block persists + notifies protocol, blackhole, delete conversation, null identity hash, error handling)ChatsScreenTest— updated allConversationContextMenucalls withonBlockUserMessagingViewModelTest,MessagingViewModelImageLoadingTest,AnnounceStreamViewModelTest— constructor updates verified🤖 Generated with Claude Code