feat(rnpath): implement the blackhole, RNPathHandler and integrate path management APIs

This commit is contained in:
2026-01-04 12:40:19 -06:00
parent 306557c473
commit 63d81a02c9
6 changed files with 322 additions and 9 deletions

View File

@@ -236,6 +236,13 @@ class ConfigManager:
)
self.message_font_size = self.IntConfig(self, "message_font_size", 14)
# blackhole integration config
self.blackhole_integration_enabled = self.BoolConfig(
self,
"blackhole_integration_enabled",
True,
)
def get(self, key: str, default_value=None) -> str | None:
return self.db.config.get(key, default_value)

View File

@@ -17,6 +17,7 @@ from meshchatx.src.backend.voicemail_manager import VoicemailManager
from meshchatx.src.backend.ringtone_manager import RingtoneManager
from meshchatx.src.backend.rncp_handler import RNCPHandler
from meshchatx.src.backend.rnstatus_handler import RNStatusHandler
from meshchatx.src.backend.rnpath_handler import RNPathHandler
from meshchatx.src.backend.rnprobe_handler import RNProbeHandler
from meshchatx.src.backend.translator_handler import TranslatorHandler
from meshchatx.src.backend.forwarding_manager import ForwardingManager
@@ -195,6 +196,9 @@ class IdentityContext:
self.rnstatus_handler = RNStatusHandler(
reticulum_instance=getattr(self.app, "reticulum", None),
)
self.rnpath_handler = RNPathHandler(
reticulum_instance=getattr(self.app, "reticulum", None),
)
self.rnprobe_handler = RNProbeHandler(
reticulum_instance=getattr(self.app, "reticulum", None),
identity=self.identity,

View File

@@ -1,5 +1,6 @@
import collections
import logging
import re
import threading
import time
from datetime import UTC, datetime
@@ -22,6 +23,10 @@ class PersistentLogHandler(logging.Handler):
self.message_counts = collections.defaultdict(int)
self.last_reset_time = time.time()
# UA and IP tracking
self.known_ips = set()
self.known_uas = set()
def set_database(self, database):
with self.lock:
self.database = database
@@ -54,8 +59,54 @@ class PersistentLogHandler(logging.Handler):
except Exception:
self.handleError(record)
def _detect_access_anomaly(self, message):
"""Detect anomalies in aiohttp access logs."""
# Regex to extract IP and User-Agent from aiohttp access log
# Format: IP [date] "GET ..." status size "referer" "User-Agent"
match = re.search(
r"^([\d\.\:]+) .* \"[^\"]+\" \d+ \d+ \"[^\"]*\" \"([^\"]+)\"", message
)
if match:
ip = match.group(1)
ua = match.group(2)
with self.lock:
is_anomaly = False
anomaly_type = None
# Detect if this is a different UA or IP from what we've seen recently
if len(self.known_ips) > 0 and ip not in self.known_ips:
is_anomaly = True
anomaly_type = "multi_ip"
if len(self.known_uas) > 0 and ua not in self.known_uas:
is_anomaly = True
if anomaly_type:
anomaly_type = "multi_ip_ua"
else:
anomaly_type = "multi_ua"
self.known_ips.add(ip)
self.known_uas.add(ua)
# Cap the tracking sets to prevent memory growth
if len(self.known_ips) > 100:
self.known_ips.clear()
if len(self.known_uas) > 100:
self.known_uas.clear()
return is_anomaly, anomaly_type
return False, None
def _detect_anomaly(self, record, message, timestamp):
# Only detect anomalies for WARNING level and above
# 1. Access anomaly detection (UA/IP) - checked for all levels of aiohttp.access
if record.name == "aiohttp.access":
is_acc_anomaly, acc_type = self._detect_access_anomaly(message)
if is_acc_anomaly:
return True, acc_type
# Only detect other anomalies for WARNING level and above
if record.levelno < logging.WARNING:
return False, None

View File

@@ -0,0 +1,62 @@
import RNS
class RNPathHandler:
def __init__(self, reticulum_instance: RNS.Reticulum):
self.reticulum = reticulum_instance
def get_path_table(self, max_hops: int = None):
table = self.reticulum.get_path_table(max_hops=max_hops)
formatted_table = []
for entry in table:
formatted_table.append(
{
"hash": entry["hash"].hex(),
"hops": entry["hops"],
"via": entry["via"].hex(),
"interface": entry["interface"],
"expires": entry["expires"],
}
)
return sorted(formatted_table, key=lambda e: (e["interface"], e["hops"]))
def get_rate_table(self):
table = self.reticulum.get_rate_table()
formatted_table = []
for entry in table:
formatted_table.append(
{
"hash": entry["hash"].hex(),
"last": entry["last"],
"timestamps": entry["timestamps"],
"rate_violations": entry["rate_violations"],
"blocked_until": entry["blocked_until"],
}
)
return sorted(formatted_table, key=lambda e: e["last"])
def drop_path(self, destination_hash: str) -> bool:
try:
dest_bytes = bytes.fromhex(destination_hash)
return self.reticulum.drop_path(dest_bytes)
except Exception:
return False
def drop_all_via(self, transport_instance_hash: str) -> bool:
try:
ti_bytes = bytes.fromhex(transport_instance_hash)
return self.reticulum.drop_all_via(ti_bytes)
except Exception:
return False
def drop_announce_queues(self):
self.reticulum.drop_announce_queues()
return True
def request_path(self, destination_hash: str):
try:
dest_bytes = bytes.fromhex(destination_hash)
RNS.Transport.request_path(dest_bytes)
return True
except Exception:
return False

View File

@@ -1,5 +1,6 @@
import time
from typing import Any
import RNS
def size_str(num, suffix="B"):
@@ -53,6 +54,19 @@ class RNStatusHandler:
"link_count": link_count,
}
blackhole_enabled = False
blackhole_sources = []
blackhole_count = 0
try:
blackhole_enabled = RNS.Reticulum.publish_blackhole_enabled()
blackhole_sources = [s.hex() for s in RNS.Reticulum.blackhole_sources()]
# Get count of blackholed identities
if self.reticulum and hasattr(self.reticulum, "get_blackholed_identities"):
blackhole_count = len(self.reticulum.get_blackholed_identities())
except Exception:
pass
interfaces = stats.get("interfaces", [])
if sorting and isinstance(sorting, str):
@@ -211,4 +225,7 @@ class RNStatusHandler:
"interfaces": formatted_interfaces,
"link_count": link_count,
"timestamp": time.time(),
"blackhole_enabled": blackhole_enabled,
"blackhole_sources": blackhole_sources,
"blackhole_count": blackhole_count,
}