feat(meshchat): improve RPC address handling and error checking; improve rule data validation for forwarding operations (found with fuzzing)

This commit is contained in:
2026-01-02 20:05:56 -06:00
parent d4ee7ac2d6
commit 5f32ae05f3

View File

@@ -928,32 +928,49 @@ class ReticulumMeshChat:
# Wait another moment for sockets to definitely be released by OS
# Also give some time for the RPC listener port to settle
print("Waiting for ports to settle...")
# Detect RPC type from reticulum instance if possible, otherwise default to both
rpc_addrs = []
if old_reticulum:
if hasattr(old_reticulum, "rpc_addr") and old_reticulum.rpc_addr:
rpc_addrs.append((old_reticulum.rpc_addr, getattr(old_reticulum, "rpc_type", "AF_INET")))
if not rpc_addrs:
# Defaults
rpc_addrs.append((("127.0.0.1", 37429), "AF_INET"))
if platform.system() == "Linux":
rpc_addrs.append(("\0rns/default/rpc", "AF_UNIX"))
for i in range(10):
await asyncio.sleep(1)
# If we're using AF_INET for RPC, we can check the port
# RNS uses 127.0.0.1:37429 by default
try:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(0.5)
# Try to bind to the port. If it fails, it's still in use.
# RNS uses this for the shared instance listener.
# We use a high port number that is unlikely to be used by anything else
# but RNS.
all_free = True
for addr, family_str in rpc_addrs:
try:
# RNS local_control_port is 37429
s.bind(("127.0.0.1", 37429))
s.close()
print("RPC port 37429 is free.")
break
except OSError:
print(f"RPC port 37429 still in use... (attempt {i + 1}/10)")
s.close()
except Exception as e:
print(f"Error checking RPC port: {e}")
import socket
family = socket.AF_INET if family_str == "AF_INET" else socket.AF_UNIX
s = socket.socket(family, socket.SOCK_STREAM)
s.settimeout(0.5)
try:
s.bind(addr)
s.close()
except OSError:
print(f"RPC addr {addr} still in use... (attempt {i+1}/10)")
s.close()
all_free = False
break
except Exception as e:
print(f"Error checking RPC addr {addr}: {e}")
if all_free:
print("All RNS ports/sockets are free.")
break
if not all_free:
raise OSError("Timeout waiting for RNS ports to be released. Cannot restart.")
# Final GC to ensure everything is released
import gc
gc.collect()
# Re-setup identity (this starts background loops again)
self.running = True
self.setup_identity(self.identity)
@@ -7138,7 +7155,11 @@ class ReticulumMeshChat:
)
elif _type == "lxmf.forwarding.rule.add":
rule_data = data["rule"]
rule_data = data.get("rule")
if not rule_data or "forward_to_hash" not in rule_data:
print("Missing rule data or forward_to_hash in lxmf.forwarding.rule.add")
return
self.database.misc.create_forwarding_rule(
identity_hash=rule_data.get("identity_hash"),
forward_to_hash=rule_data["forward_to_hash"],
@@ -7155,26 +7176,28 @@ class ReticulumMeshChat:
)
elif _type == "lxmf.forwarding.rule.delete":
rule_id = data["id"]
self.database.misc.delete_forwarding_rule(rule_id)
# notify updated
AsyncUtils.run_async(
self.on_websocket_data_received(
client,
{"type": "lxmf.forwarding.rules.get"},
),
)
rule_id = data.get("id")
if rule_id is not None:
self.database.misc.delete_forwarding_rule(rule_id)
# notify updated
AsyncUtils.run_async(
self.on_websocket_data_received(
client,
{"type": "lxmf.forwarding.rules.get"},
),
)
elif _type == "lxmf.forwarding.rule.toggle":
rule_id = data["id"]
self.database.misc.toggle_forwarding_rule(rule_id)
# notify updated
AsyncUtils.run_async(
self.on_websocket_data_received(
client,
{"type": "lxmf.forwarding.rules.get"},
),
)
rule_id = data.get("id")
if rule_id is not None:
self.database.misc.toggle_forwarding_rule(rule_id)
# notify updated
AsyncUtils.run_async(
self.on_websocket_data_received(
client,
{"type": "lxmf.forwarding.rules.get"},
),
)
# handle ingesting an lxmf uri (paper message)
elif _type == "lxm.ingest_uri":
@@ -8648,6 +8671,11 @@ class ReticulumMeshChat:
app_data,
announce_packet_hash,
):
# check if announced identity or its hash is missing
if not announced_identity or not announced_identity.hash:
print(f"Dropping announce with missing identity or hash: {RNS.prettyhexrep(destination_hash)}")
return
# check if source is blocked - drop announce and path if blocked
identity_hash = announced_identity.hash.hex()
if self.is_destination_blocked(identity_hash):