Critical fixes: - Fix memory leak: RSSI polling now stopped in onCleared() - Fix silent BLE scan errors: user-friendly error messages added - Fix double-check locking bug in Python RNode initialization - Fix interface registration order: start() before Transport register - Fix race condition: use threading.Event for read loop control - Fix write retry: implement exponential backoff (0.3s, 1s, 3s) - Fix BLE write latch race: null check prevents stale callbacks - Fix MTU request hang: 2-second timeout falls back to discoverServices Tests added: - RSSI polling cancellation test - BLE scan error handling test - Thread safety tests for read loop - Write retry exponential backoff tests Also includes ktlint format auto-fixes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
9.5 KiB
RPC Authentication Error in Shared Instance Mode
Overview
When Columba connects to a shared Reticulum instance (e.g., Sideband), message delivery fails with RPC authentication errors. This document explains the root cause and provides solutions.
Key Finding: RPC is NOT Required for Message Delivery
Core packet routing uses LocalClientInterface socket connections directly - NOT RPC.
The RPC subsystem is only used for:
- Statistics queries (RSSI, SNR, link quality)
- Path table queries
- Management operations (drop paths, clear queues)
However, a bug in RNS causes message delivery to crash when RPC authentication fails.
LXMF Delivery Methods
| Method | Description | Max Size | RPC Required? |
|---|---|---|---|
| OPPORTUNISTIC | Single encrypted packet, no link | ~295 bytes | No |
| DIRECT | Link-based, reliable delivery | Unlimited | No |
| PROPAGATED | Store-and-forward via propagation node | Unlimited | No |
| PAPER | QR code/URI for manual transfer | ~1760 bytes | No |
All methods deliver through LXMRouter.lxmf_delivery() → delivery callback. None require RPC.
The Bug: RPC Auth Error Crashes Message Delivery
Evidence from Logcat
12-07 23:45:11.910 I python.stdout: [2025-12-07 23:45:11] [Error] Traceback (most recent call last):
12-07 23:45:11.910 I python.stdout: File ".../RNS/Interfaces/LocalInterface.py", line 193, in process_incoming
12-07 23:45:11.910 I python.stdout: File ".../RNS/Transport.py", line 1889, in inbound
12-07 23:45:11.910 I python.stdout: File ".../RNS/Link.py", line 995, in receive
12-07 23:45:11.910 I python.stdout: File ".../RNS/Link.py", line 837, in __update_phy_stats
12-07 23:45:11.910 I python.stdout: File ".../RNS/Reticulum.py", line 1280, in get_packet_rssi
12-07 23:45:11.910 I python.stdout: File ".../RNS/Reticulum.py", line 969, in get_rpc_client
12-07 23:45:11.911 I python.stdout: multiprocessing.context.AuthenticationError: digest sent was rejected
12-07 23:45:13.279 I sidebandservice: [Error] An error ocurred while handling RPC call from local client: digest received was wrong
Exact Call Chain
1. Packet arrives at LocalClientInterface (connected to Sideband's shared instance)
↓
2. LocalInterface.process_incoming() [LocalInterface.py:193]
- Receives raw packet bytes from shared instance socket
↓
3. Transport.inbound() [Transport.py:1889]
- Routes packet to appropriate handler
- For link-related packets, calls link.receive()
↓
4. Link.receive() [Link.py:995]
- Processes the link packet (DATA, LINKIDENTIFY, REQUEST, etc.)
- Calls __update_phy_stats() to record signal quality
↓
5. Link.__update_phy_stats() [Link.py:837]
- Called with force_update=True at 17 different sites in receive()
- Tries to get RSSI/SNR/Q from the packet
- If connected to shared instance, makes RPC call
↓
6. Reticulum.get_packet_rssi() [Reticulum.py:1280]
- Makes RPC call to shared instance to query cached RSSI value
↓
7. Reticulum.get_rpc_client() [Reticulum.py:969]
- Creates RPC connection with authkey
- AuthenticationError: Columba's authkey doesn't match Sideband's
Root Cause
Columba and Sideband have different RPC keys because:
- Each app has its own config directory (Android sandboxing)
- RPC keys are derived from app-specific identity private keys
- Without explicit key sharing, authentication fails
Why This Crashes Message Delivery
The __update_phy_stats() method does NOT have exception handling:
# Link.py lines 833-850 (simplified)
def __update_phy_stats(self, packet, force_update=False):
if self.__track_phy_stats or force_update:
if RNS.Reticulum.get_instance().is_connected_to_shared_instance:
# NO TRY-CATCH HERE!
self.rssi = RNS.Reticulum.get_instance().get_packet_rssi(packet.packet_hash)
self.snr = RNS.Reticulum.get_instance().get_packet_snr(packet.packet_hash)
self.q = RNS.Reticulum.get_instance().get_packet_q(packet.packet_hash)
When get_packet_rssi() raises AuthenticationError, it propagates up through:
__update_phy_stats()→receive()→Transport.inbound()→process_incoming()
This crashes the entire packet processing chain.
Why force_update=True Is Used
Even though __track_phy_stats defaults to False, certain call sites in Link.receive() use force_update=True:
- Line 218: Incoming link request acceptance
- Lines 995, 1031, 1040, 1053, 1060, 1064, 1069: Various DATA packet types
- Lines 1107, 1129, 1138, 1147, 1167, 1176, 1185: RESOURCE_ADV, LINKCLOSE, etc.
This is automatic RNS behavior - Columba does not explicitly request signal stats.
Can RPC Key Enable Interface Configuration?
NO - Even with valid RPC key, shared instance clients cannot configure interfaces:
# From Reticulum.py _add_interface()
if not self.is_connected_to_shared_instance:
# Process interface configuration
Interface configuration is blocked for connected clients. This is by design - the shared instance owns the interfaces.
Solutions
Option 1: Patch RNS in Columba's Bundled Copy
Add exception handling in Link.__update_phy_stats():
def __update_phy_stats(self, packet, force_update=False):
if self.__track_phy_stats or force_update:
try:
if RNS.Reticulum.get_instance().is_connected_to_shared_instance:
self.rssi = RNS.Reticulum.get_instance().get_packet_rssi(packet.packet_hash)
self.snr = RNS.Reticulum.get_instance().get_packet_snr(packet.packet_hash)
self.q = RNS.Reticulum.get_instance().get_packet_q(packet.packet_hash)
else:
# ... existing local cache lookup
except Exception as e:
RNS.log(f"Could not update physical layer stats: {e}", RNS.LOG_DEBUG)
# Stats remain None, but packet processing continues
Pros: Immediate fix, no user action required Cons: Diverges from upstream RNS
Option 2: Configure RPC Key
Allow users to paste the RPC key from Sideband (Settings → Connectivity → "Share Instance Access").
Pros: Enables full stats functionality Cons: Requires user action, key management
Option 3: Both Approaches
Patch for resilience + allow RPC key for full functionality.
Pros: Best of both worlds Cons: More implementation work
Option 4: Upstream PR
Submit fix to Reticulum repository.
Pros: Fixes for everyone Cons: Dependent on upstream acceptance timeline
Sideband RPC Key Export
Location in Sideband: Settings → Connectivity → "Share Instance Access"
The exported config includes:
shared_instance_type = tcp
rpc_key = <hex_key>
Why Other RNS Clients Don't Have This Problem
NomadNet & MeshChat Approach
Both NomadNet and reticulum-meshchat use a simple initialization pattern:
# NomadNet (nomadnet/NomadNetworkApp.py:95)
self.rns = RNS.Reticulum(configdir=rnsconfigdir)
# MeshChat (meshchat.py:113-115)
self.reticulum = RNS.Reticulum(reticulum_config_dir)
Why they work: These are typically run on Linux/desktop systems where:
- All apps can share the same
~/.reticulumconfig directory - The RPC key is derived from the same identity file
- RPC authentication succeeds because both sides use the same key
Sideband's Android Solution
Sideband explicitly handles the Android sandboxing problem:
File: /home/tyler/repos/Sideband/sbapp/sideband/core.py:607-608
self.identity = RNS.Identity.from_file(self.identity_path)
self.rpc_key = RNS.Identity.full_hash(self.identity.get_private_key())
File: /home/tyler/repos/Sideband/sbapp/main.py:3914-3925 - Export feature:
rpc_string = "shared_instance_type = tcp\n"
rpc_string += "rpc_key = " + RNS.hexrep(self.sideband.reticulum.rpc_key, delimit=False)
Sideband provides a UI to export this configuration, which other apps can paste into their Reticulum config.
The Columba Problem
Columba is unique because:
- Android sandbox: Columba has its own app directory, can't read Sideband's config
- Bundled Python: Uses Chaquopy with its own RNS installation
- Own identity: Columba creates its own identity file (different from Sideband's)
- Derived RPC key: RNS derives RPC key from identity:
Identity.full_hash(private_key)
When Columba connects to Sideband's shared instance:
- Data routing works (LocalClientInterface uses raw sockets)
- RPC fails (Columba's derived key != Sideband's derived key)
RPC Key Configuration in RNS
File: /home/tyler/repos/Reticulum/RNS/Reticulum.py:472-478
if option == "rpc_key":
try:
value = bytes.fromhex(self.config["reticulum"][option])
self.rpc_key = value
except Exception as e:
RNS.log("Invalid shared instance RPC key specified, falling back to default key", RNS.LOG_ERROR)
self.rpc_key = None
RNS supports explicit RPC key configuration for exactly this scenario - when apps can't share config directories.
Summary
| Aspect | Status |
|---|---|
| Message delivery without RPC | Should work (bug prevents it) |
| Root cause | Missing exception handling in __update_phy_stats() |
| Why other clients work | They share config directories or explicitly configure RPC key |
| Columba's issue | Android sandbox prevents config sharing, RPC key mismatch |
| Interface configuration | Always disabled for shared instance clients |
| Recommended fix | Patch RNS + support RPC key configuration in UI |