2025-12-31 13:50:49 +01:00
import os
2026-01-17 17:09:01 +01:00
import re
2025-12-31 01:07:08 +01:00
import RNS
import time
2026-01-17 18:47:08 +01:00
import random
2025-12-31 01:07:08 +01:00
import threading
2026-01-17 17:09:01 +01:00
import ipaddress
2026-01-04 00:50:35 +01:00
import subprocess
2026-05-10 00:29:48 +02:00
from threading import Lock
2025-12-31 01:07:08 +01:00
from . vendor import umsgpack as msgpack
2026-07-20 12:57:49 +02:00
from collections import deque
2025-12-31 01:07:08 +01:00
NAME = 0xFF
2026-01-02 17:16:24 +01:00
TRANSPORT_ID = 0xFE
2025-12-31 01:07:08 +01:00
INTERFACE_TYPE = 0x00
2025-12-31 15:23:43 +01:00
TRANSPORT = 0x01
REACHABLE_ON = 0x02
LATITUDE = 0x03
LONGITUDE = 0x04
HEIGHT = 0x05
PORT = 0x06
IFAC_NETNAME = 0x07
IFAC_NETKEY = 0x08
FREQUENCY = 0x09
BANDWIDTH = 0x0A
SPREADINGFACTOR = 0x0B
CODINGRATE = 0x0C
MODULATION = 0x0D
2026-01-01 14:49:40 +01:00
CHANNEL = 0x0E
2026-08-16 00:29:14 +02:00
OP_ADDR = 0xF0
2025-12-31 01:07:08 +01:00
APP_NAME = " rnstransport "
class InterfaceAnnouncer ( ) :
JOB_INTERVAL = 60
2026-07-20 17:24:51 +02:00
DEFAULT_STAMP_VALUE = 16
2025-12-31 01:07:08 +01:00
WORKBLOCK_EXPAND_ROUNDS = 20
2026-01-01 14:49:40 +01:00
DISCOVERABLE_INTERFACE_TYPES = [ " BackboneInterface " , " TCPServerInterface " , " TCPClientInterface " ,
" RNodeInterface " , " WeaveInterface " , " I2PInterface " , " KISSInterface " ]
2025-12-31 01:07:08 +01:00
def __init__ ( self , owner ) :
import importlib . util
if importlib . util . find_spec ( ' LXMF ' ) != None : from LXMF import LXStamper
else :
RNS . log ( " Using on-network interface discovery requires the LXMF module to be installed. " , RNS . LOG_CRITICAL )
RNS . log ( " You can install it with the command: pip install lxmf " , RNS . LOG_CRITICAL )
RNS . panic ( )
self . owner = owner
self . should_run = False
self . job_interval = self . JOB_INTERVAL
self . stamper = LXStamper
self . stamp_cache = { }
2026-01-02 17:16:24 +01:00
if self . owner . has_network_identity ( ) : identity = self . owner . network_identity
else : identity = self . owner . identity
self . discovery_destination = RNS . Destination ( identity , RNS . Destination . IN , RNS . Destination . SINGLE ,
2025-12-31 01:07:08 +01:00
APP_NAME , " discovery " , " interface " )
def start ( self ) :
if not self . should_run :
self . should_run = True
threading . Thread ( target = self . job , daemon = True ) . start ( )
def stop ( self ) : self . should_run = False
def job ( self ) :
while self . should_run :
time . sleep ( self . job_interval )
try :
now = time . time ( )
due_interfaces = [ i for i in self . owner . interfaces if i . supports_discovery and i . discoverable and now > ( i . last_discovery_announce + i . discovery_announce_interval ) ]
due_interfaces . sort ( key = lambda i : now - i . last_discovery_announce , reverse = True )
if len ( due_interfaces ) > 0 :
selected_interface = due_interfaces [ 0 ]
selected_interface . last_discovery_announce = time . time ( )
2026-01-01 14:49:40 +01:00
RNS . log ( f " Preparing interface discovery announce for { selected_interface . name } " , RNS . LOG_DEBUG )
2025-12-31 01:07:08 +01:00
app_data = self . get_interface_announce_data ( selected_interface )
if not app_data : RNS . log ( f " Could not generate interface discovery announce data for { selected_interface . name } " , RNS . LOG_ERROR )
else :
2026-01-01 14:49:40 +01:00
RNS . log ( f " Sending interface discovery announce for { selected_interface . name } with { len ( app_data ) } B payload " , RNS . LOG_DEBUG )
2025-12-31 01:07:08 +01:00
self . discovery_destination . announce ( app_data = app_data )
except Exception as e :
RNS . log ( f " Error while preparing interface discovery announces: { e } " , RNS . LOG_ERROR )
RNS . trace_exception ( e )
2025-12-31 11:25:20 +01:00
def sanitize ( self , in_str ) :
2026-05-13 10:34:58 +02:00
if in_str == None : return None
2025-12-31 11:25:20 +01:00
sanitized = in_str . replace ( " \n " , " " )
sanitized = sanitized . replace ( " \r " , " " )
sanitized = sanitized . strip ( )
return sanitized
2025-12-31 01:07:08 +01:00
def get_interface_announce_data ( self , interface ) :
interface_type = type ( interface ) . __name__
2026-01-02 17:16:24 +01:00
stamp_value = interface . discovery_stamp_value if interface . discovery_stamp_value else self . DEFAULT_STAMP_VALUE
2025-12-31 01:07:08 +01:00
if not interface_type in self . DISCOVERABLE_INTERFACE_TYPES : return None
else :
2026-07-14 20:41:28 +02:00
if not RNS . vendor . platformutils . is_windows ( ) and interface . discovery_location :
try :
discovery_location = self . sanitize ( interface . discovery_location )
exec_path = os . path . expanduser ( discovery_location )
if os . path . isfile ( exec_path ) and os . access ( exec_path , os . X_OK ) :
RNS . log ( f " Evaluating discovery location from executable at { exec_path } " , RNS . LOG_DEBUG )
exec_result = subprocess . run ( [ exec_path ] , stdout = subprocess . PIPE )
exec_stdout = exec_result . stdout . decode ( " utf-8 " )
if exec_result . returncode != 0 : raise ValueError ( " Non-zero exit code from subprocess " )
discovery_location = self . sanitize ( exec_stdout )
location_components = discovery_location . replace ( " " , " " ) . split ( " , " )
if len ( location_components ) != 3 : raise ValueError ( f " Invalid location component count: { len ( location_components ) } " )
dlat = float ( location_components [ 0 ] )
dlon = float ( location_components [ 1 ] )
dhgt = float ( location_components [ 2 ] )
if dlat < - 90 or dlat > 90 : raise ValueError ( f " Invalid latitude: { dlat } " )
if dlon < - 180 or dlat > 180 : raise ValueError ( f " Invalid longitude: { dlon } " )
if dhgt < - 4000 or dhgt > 1e6 : raise ValueError ( f " Invalid height: { dhgt } " )
interface . discovery_latitude = dlat
interface . discovery_longitude = dlon
interface . discovery_height = dhgt
except Exception as e :
2026-08-16 00:29:14 +02:00
RNS . log ( f " Error while getting discovery location from executable at { interface . reachable_on } : { e } " , RNS . LOG_ERROR )
2026-07-14 20:41:28 +02:00
RNS . log ( f " Aborting discovery announce " , RNS . LOG_ERROR )
return None
2026-01-03 02:20:24 +01:00
flags = 0x00
2026-01-01 14:49:40 +01:00
info = { INTERFACE_TYPE : interface_type ,
TRANSPORT : RNS . Reticulum . transport_enabled ( ) ,
2026-01-02 17:16:24 +01:00
TRANSPORT_ID : RNS . Transport . identity . hash ,
2026-01-01 14:49:40 +01:00
NAME : self . sanitize ( interface . discovery_name ) ,
LATITUDE : interface . discovery_latitude ,
LONGITUDE : interface . discovery_longitude ,
HEIGHT : interface . discovery_height }
2025-12-31 01:07:08 +01:00
2026-08-16 00:29:14 +02:00
if interface . discovery_lxmf_address : info [ OP_ADDR ] = interface . discovery_lxmf_address
2026-04-12 19:57:34 +02:00
if interface_type == " TCPClientInterface " and not interface . kiss_framing :
RNS . log ( f " Invalid interface discovery configuration for { interface } , aborting discovery announce " , RNS . LOG_ERROR )
return None
2026-03-12 00:24:09 +01:00
if interface_type in [ " BackboneInterface " , " TCPServerInterface " ] :
2026-08-16 00:29:14 +02:00
if not interface . reachable_on : return None
2026-03-12 00:24:09 +01:00
reachable_on = self . sanitize ( interface . reachable_on )
if not RNS . vendor . platformutils . is_windows ( ) :
try :
exec_path = os . path . expanduser ( reachable_on )
if os . path . isfile ( exec_path ) and os . access ( exec_path , os . X_OK ) :
RNS . log ( f " Evaluating reachable_on from executable at { exec_path } " , RNS . LOG_DEBUG )
exec_result = subprocess . run ( [ exec_path ] , stdout = subprocess . PIPE )
exec_stdout = exec_result . stdout . decode ( " utf-8 " )
if exec_result . returncode != 0 : raise ValueError ( " Non-zero exit code from subprocess " )
reachable_on = self . sanitize ( exec_stdout )
if not ( is_ip_address ( reachable_on ) or is_hostname ( reachable_on ) ) :
raise ValueError ( f " Valid IP address or hostname was not found in external script output \" { reachable_on } \" " )
except Exception as e :
RNS . log ( f " Error while getting reachable_on from executable at { interface . reachable_on } : { e } " , RNS . LOG_ERROR )
RNS . log ( f " Aborting discovery announce " , RNS . LOG_ERROR )
return None
if not ( is_ip_address ( reachable_on ) or is_hostname ( reachable_on ) ) :
RNS . log ( f " The configured reachable_on parameter \" { reachable_on } \" for { interface } is not a valid IP address or hostname " , RNS . LOG_ERROR )
2026-01-04 00:50:35 +01:00
RNS . log ( f " Aborting discovery announce " , RNS . LOG_ERROR )
return None
info [ REACHABLE_ON ] = reachable_on
2025-12-31 01:07:08 +01:00
info [ PORT ] = interface . bind_port
if interface_type == " I2PInterface " and interface . connectable and interface . b32 :
info [ REACHABLE_ON ] = interface . b32
if interface_type == " RNodeInterface " :
info [ FREQUENCY ] = interface . frequency
info [ BANDWIDTH ] = interface . bandwidth
info [ SPREADINGFACTOR ] = interface . sf
info [ CODINGRATE ] = interface . cr
2026-01-01 14:49:40 +01:00
if interface_type == " WeaveInterface " :
info [ FREQUENCY ] = interface . discovery_frequency
info [ BANDWIDTH ] = interface . discovery_bandwidth
info [ CHANNEL ] = interface . discovery_channel
info [ MODULATION ] = interface . discovery_modulation
2025-12-31 01:07:08 +01:00
if interface_type == " KISSInterface " or ( interface_type == " TCPClientInterface " and interface . kiss_framing ) :
info [ INTERFACE_TYPE ] = " KISSInterface "
info [ FREQUENCY ] = interface . discovery_frequency
info [ BANDWIDTH ] = interface . discovery_bandwidth
2025-12-31 11:25:20 +01:00
info [ MODULATION ] = self . sanitize ( interface . discovery_modulation )
2025-12-31 01:07:08 +01:00
if interface . discovery_publish_ifac == True :
2025-12-31 11:25:20 +01:00
info [ IFAC_NETNAME ] = self . sanitize ( interface . ifac_netname )
info [ IFAC_NETKEY ] = self . sanitize ( interface . ifac_netkey )
2025-12-31 01:07:08 +01:00
2026-01-02 17:16:24 +01:00
packed = msgpack . packb ( info )
infohash = RNS . Identity . full_hash ( packed )
2025-12-31 01:07:08 +01:00
2026-01-03 02:20:24 +01:00
if infohash in self . stamp_cache : stamp = self . stamp_cache [ infohash ]
2025-12-31 01:07:08 +01:00
else : stamp , v = self . stamper . generate_stamp ( infohash , stamp_cost = stamp_value , expand_rounds = self . WORKBLOCK_EXPAND_ROUNDS )
if not stamp : return None
2026-01-03 02:20:24 +01:00
else : self . stamp_cache [ infohash ] = stamp
if interface . discovery_encrypt :
flags | = InterfaceAnnounceHandler . FLAG_ENCRYPTED
if not self . owner . has_network_identity ( ) :
RNS . log ( f " Discovery encryption requested for { interface } , but no network identity configured. Aborting discovery announce. " , RNS . LOG_ERROR )
return None
else : payload = self . owner . network_identity . encrypt ( packed + stamp )
else : payload = packed + stamp
return bytes ( [ flags ] ) + payload
2025-12-31 01:07:08 +01:00
class InterfaceAnnounceHandler :
2026-01-02 17:16:24 +01:00
FLAG_SIGNED = 0b00000001
FLAG_ENCRYPTED = 0b00000010
2025-12-31 01:07:08 +01:00
def __init__ ( self , required_value = InterfaceAnnouncer . DEFAULT_STAMP_VALUE , callback = None ) :
import importlib . util
if importlib . util . find_spec ( ' LXMF ' ) != None : from LXMF import LXStamper
else :
RNS . log ( " Using on-network interface discovery requires the LXMF module to be installed. " , RNS . LOG_CRITICAL )
RNS . log ( " You can install it with the command: pip install lxmf " , RNS . LOG_CRITICAL )
RNS . panic ( )
2026-07-20 17:21:41 +02:00
self . aspect_filter = APP_NAME + " .discovery.interface "
self . required_value = required_value
self . callback = callback
self . stamper = LXStamper
self . valid_cache = { }
self . valid_cache_max = 2048
self . invalid_cache = deque ( maxlen = 2048 )
self . validation_lock = threading . Lock ( )
2025-12-31 01:07:08 +01:00
2026-05-06 04:24:07 +02:00
@staticmethod
def sanitize_name ( name ) :
if not name : return None
name = name . encode ( " ascii " , " ignore " ) . decode ( " ascii " ) . strip ( )
for i in [ 5 , 3 , 2 ] : name = name . replace ( " " * i , " " )
while len ( name ) and name [ 0 ] not in san_map : name = name [ 1 : ]
while len ( name ) and name [ - 1 ] not in san_map + " ) " : name = name [ : - 1 ]
return name
2025-12-31 01:07:08 +01:00
def received_announce ( self , destination_hash , announced_identity , app_data ) :
try :
2026-01-02 21:03:18 +01:00
discovery_sources = RNS . Reticulum . interface_discovery_sources ( )
if discovery_sources and not announced_identity . hash in discovery_sources :
RNS . log ( f " Interface discovered from non-authorized network identity { RNS . prettyhexrep ( announced_identity . hash ) } , ignoring " , RNS . LOG_DEBUG )
return
2025-12-31 17:12:15 +01:00
if app_data and len ( app_data ) > self . stamper . STAMP_SIZE + 1 :
2026-01-02 17:16:24 +01:00
flags = app_data [ 0 ]
2025-12-31 17:12:15 +01:00
app_data = app_data [ 1 : ]
2026-01-02 17:16:24 +01:00
signed = flags & self . FLAG_SIGNED
encrypted = flags & self . FLAG_ENCRYPTED
2026-07-20 12:57:49 +02:00
fullhash = RNS . Identity . full_hash ( app_data )
if fullhash in self . invalid_cache :
RNS . log ( f " Ignored previously discovered interface with insufficient stamp value " , RNS . LOG_PATHING ) if RNS . sl ( RNS . LOG_PATHING ) else None
return
2026-01-02 17:16:24 +01:00
2026-07-20 17:21:41 +02:00
if fullhash in self . valid_cache :
RNS . log ( f " Discovery announce cache hit " , RNS . LOG_PATHING ) if RNS . sl ( RNS . LOG_PATHING ) else None
cache_entry = self . valid_cache [ fullhash ]
stamp = cache_entry [ " stamp " ]
packed = cache_entry [ " packed " ]
value = cache_entry [ " value " ]
valid = cache_entry [ " valid " ]
else :
if encrypted :
if not RNS . Transport . has_network_identity ( ) : return
app_data = RNS . Transport . network_identity . decrypt ( app_data )
if not app_data : return
if self . validation_lock . locked ( ) :
RNS . log ( f " Dropping received interface discovery announce, already validating other discovery stamp " , RNS . LOG_DEBUG ) if RNS . sl ( RNS . LOG_DEBUG ) else None
return
with self . validation_lock :
stamp = app_data [ - self . stamper . STAMP_SIZE : ]
packed = app_data [ : - self . stamper . STAMP_SIZE ]
infohash = RNS . Identity . full_hash ( packed )
workblock = self . stamper . stamp_workblock ( infohash , expand_rounds = InterfaceAnnouncer . WORKBLOCK_EXPAND_ROUNDS )
value = self . stamper . stamp_value ( workblock , stamp )
valid = self . stamper . stamp_valid ( stamp , self . required_value , workblock )
self . valid_cache [ fullhash ] = { " valid " : valid , " value " : value , " packed " : packed , " stamp " : stamp }
while len ( self . valid_cache ) > self . valid_cache_max : self . valid_cache . pop ( next ( iter ( self . valid_cache ) ) )
2025-12-31 13:50:49 +01:00
if not valid :
2026-07-18 23:03:35 +02:00
RNS . log ( f " Ignored discovered interface with insufficient stamp value { value } " , RNS . LOG_DEBUG ) if RNS . sl ( RNS . LOG_DEBUG ) else None
2026-07-20 12:57:49 +02:00
self . invalid_cache . append ( fullhash )
2025-12-31 13:50:49 +01:00
return
2025-12-31 01:07:08 +01:00
if value < self . required_value : RNS . log ( f " Ignored discovered interface with stamp value { value } " , RNS . LOG_DEBUG )
else :
info = None
unpacked = msgpack . unpackb ( packed )
if INTERFACE_TYPE in unpacked :
2026-05-06 04:24:07 +02:00
interface_type = unpacked [ INTERFACE_TYPE ]
name = self . sanitize_name ( unpacked [ NAME ] )
if type ( unpacked [ TRANSPORT ] ) != bool : raise ValueError ( " Invalid data in transport field of announce " )
if type ( unpacked [ LATITUDE ] ) not in [ type ( None ) , float ] : raise ValueError ( " Invalid data in latitude field of announce " )
if type ( unpacked [ LONGITUDE ] ) not in [ type ( None ) , float ] : raise ValueError ( " Invalid data in longitude field of announce " )
if type ( unpacked [ HEIGHT ] ) not in [ type ( None ) , float ] : raise ValueError ( " Invalid data in height field of announce " )
if len ( unpacked [ TRANSPORT_ID ] ) != RNS . Identity . TRUNCATED_HASHLENGTH / / 8 : raise ValueError ( " Invalid data in transport_id field of announce " )
if not interface_type in InterfaceAnnouncer . DISCOVERABLE_INTERFACE_TYPES :
raise ValueError ( " Invalid interface type in announce data " )
if REACHABLE_ON in unpacked :
if not ( is_ip_address ( unpacked [ REACHABLE_ON ] ) or is_hostname ( unpacked [ REACHABLE_ON ] ) ) :
raise ValueError ( " Invalid data in reachable_on field of announce " )
2026-01-02 17:16:24 +01:00
info = { " type " : interface_type ,
" transport " : unpacked [ TRANSPORT ] ,
2026-05-06 04:24:07 +02:00
" name " : name or f " Discovered { interface_type } " ,
2026-01-02 17:16:24 +01:00
" received " : time . time ( ) ,
" stamp " : stamp ,
" value " : value ,
" transport_id " : RNS . hexrep ( unpacked [ TRANSPORT_ID ] , delimit = False ) ,
" network_id " : RNS . hexrep ( announced_identity . hash , delimit = False ) ,
" hops " : RNS . Transport . hops_to ( destination_hash ) ,
" latitude " : unpacked [ LATITUDE ] ,
" longitude " : unpacked [ LONGITUDE ] ,
" height " : unpacked [ HEIGHT ] }
2025-12-31 01:07:08 +01:00
2026-05-06 04:24:07 +02:00
if IFAC_NETNAME in unpacked : info [ " ifac_netname " ] = str ( unpacked [ IFAC_NETNAME ] )
if IFAC_NETKEY in unpacked : info [ " ifac_netkey " ] = str ( unpacked [ IFAC_NETKEY ] )
2025-12-31 01:07:08 +01:00
if interface_type in [ " BackboneInterface " , " TCPServerInterface " ] :
2026-07-24 02:29:51 +02:00
backbone_support = not RNS . vendor . platformutils . is_windows ( ) and not RNS . vendor . platformutils . is_darwin ( )
2025-12-31 01:07:08 +01:00
info [ " reachable_on " ] = unpacked [ REACHABLE_ON ]
info [ " port " ] = unpacked [ PORT ]
2025-12-31 14:51:56 +01:00
connection_interface = " BackboneInterface " if backbone_support else " TCPClientInterface "
2025-12-31 01:07:08 +01:00
remote_str = " remote " if backbone_support else " target_host "
cfg_name = info [ " name " ]
cfg_remote = info [ " reachable_on " ]
cfg_port = info [ " port " ]
2026-01-02 17:16:24 +01:00
cfg_identity = info [ " transport_id " ]
2025-12-31 01:07:08 +01:00
cfg_netname = info [ " ifac_netname " ] if " ifac_netname " in info else None
cfg_netkey = info [ " ifac_netkey " ] if " ifac_netkey " in info else None
cfg_netname_str = f " \n network_name = { cfg_netname } " if cfg_netname else " "
cfg_netkey_str = f " \n passphrase = { cfg_netkey } " if cfg_netkey else " "
cfg_identity_str = f " \n transport_identity = { cfg_identity } "
2025-12-31 13:50:49 +01:00
info [ " config_entry " ] = f " [[ { cfg_name } ]] \n type = { connection_interface } \n enabled = yes \n { remote_str } = { cfg_remote } \n target_port = { cfg_port } { cfg_identity_str } { cfg_netname_str } { cfg_netkey_str } "
2025-12-31 01:07:08 +01:00
if interface_type == " I2PInterface " :
info [ " reachable_on " ] = unpacked [ REACHABLE_ON ]
cfg_name = info [ " name " ]
cfg_remote = info [ " reachable_on " ]
2026-01-02 17:16:24 +01:00
cfg_identity = info [ " transport_id " ]
2025-12-31 01:07:08 +01:00
cfg_netname = info [ " ifac_netname " ] if " ifac_netname " in info else None
cfg_netkey = info [ " ifac_netkey " ] if " ifac_netkey " in info else None
cfg_netname_str = f " \n network_name = { cfg_netname } " if cfg_netname else " "
cfg_netkey_str = f " \n passphrase = { cfg_netkey } " if cfg_netkey else " "
cfg_identity_str = f " \n transport_identity = { cfg_identity } "
2025-12-31 13:50:49 +01:00
info [ " config_entry " ] = f " [[ { cfg_name } ]] \n type = I2PInterface \n enabled = yes \n peers = { cfg_remote } { cfg_identity_str } { cfg_netname_str } { cfg_netkey_str } "
2025-12-31 01:07:08 +01:00
if interface_type == " RNodeInterface " :
info [ " frequency " ] = unpacked [ FREQUENCY ]
info [ " bandwidth " ] = unpacked [ BANDWIDTH ]
info [ " sf " ] = unpacked [ SPREADINGFACTOR ]
info [ " cr " ] = unpacked [ CODINGRATE ]
cfg_name = info [ " name " ]
cfg_frequency = info [ " frequency " ]
cfg_bandwidth = info [ " bandwidth " ]
cfg_sf = info [ " sf " ]
cfg_cr = info [ " cr " ]
2026-01-02 17:16:24 +01:00
cfg_identity = info [ " transport_id " ]
2025-12-31 01:07:08 +01:00
cfg_netname = info [ " ifac_netname " ] if " ifac_netname " in info else None
cfg_netkey = info [ " ifac_netkey " ] if " ifac_netkey " in info else None
cfg_netname_str = f " \n network_name = { cfg_netname } " if cfg_netname else " "
cfg_netkey_str = f " \n passphrase = { cfg_netkey } " if cfg_netkey else " "
cfg_identity_str = f " \n transport_identity = { cfg_identity } "
2025-12-31 13:50:49 +01:00
info [ " config_entry " ] = f " [[ { cfg_name } ]] \n type = RNodeInterface \n enabled = yes \n port = \n frequency = { cfg_frequency } \n bandwidth = { cfg_bandwidth } \n spreadingfactor = { cfg_sf } \n codingrate = { cfg_cr } \n txpower = { cfg_netname_str } { cfg_netkey_str } "
2025-12-31 01:07:08 +01:00
2026-01-01 14:49:40 +01:00
if interface_type == " WeaveInterface " :
info [ " frequency " ] = unpacked [ FREQUENCY ]
info [ " bandwidth " ] = unpacked [ BANDWIDTH ]
info [ " channel " ] = unpacked [ CHANNEL ]
info [ " modulation " ] = unpacked [ MODULATION ]
cfg_name = info [ " name " ]
2026-01-02 17:16:24 +01:00
cfg_identity = info [ " transport_id " ]
2026-01-01 14:49:40 +01:00
cfg_netname = info [ " ifac_netname " ] if " ifac_netname " in info else None
cfg_netkey = info [ " ifac_netkey " ] if " ifac_netkey " in info else None
cfg_netname_str = f " \n network_name = { cfg_netname } " if cfg_netname else " "
cfg_netkey_str = f " \n passphrase = { cfg_netkey } " if cfg_netkey else " "
cfg_identity_str = f " \n transport_identity = { cfg_identity } "
info [ " config_entry " ] = f " [[ { cfg_name } ]] \n type = WeaveInterface \n enabled = yes \n port = { cfg_netname_str } { cfg_netkey_str } "
2025-12-31 01:07:08 +01:00
if interface_type == " KISSInterface " :
info [ " frequency " ] = unpacked [ FREQUENCY ]
info [ " bandwidth " ] = unpacked [ BANDWIDTH ]
info [ " modulation " ] = unpacked [ MODULATION ]
cfg_name = info [ " name " ]
cfg_frequency = info [ " frequency " ]
cfg_bandwidth = info [ " bandwidth " ]
cfg_modulation = info [ " modulation " ]
2026-01-02 17:16:24 +01:00
cfg_identity = info [ " transport_id " ]
2025-12-31 01:07:08 +01:00
cfg_netname = info [ " ifac_netname " ] if " ifac_netname " in info else None
cfg_netkey = info [ " ifac_netkey " ] if " ifac_netkey " in info else None
cfg_netname_str = f " \n network_name = { cfg_netname } " if cfg_netname else " "
cfg_netkey_str = f " \n passphrase = { cfg_netkey } " if cfg_netkey else " "
cfg_identity_str = f " \n transport_identity = { cfg_identity } "
2025-12-31 13:50:49 +01:00
info [ " config_entry " ] = f " [[ { cfg_name } ]] \n type = KISSInterface \n enabled = yes \n port = \n # Frequency: { cfg_frequency } \n # Bandwidth: { cfg_bandwidth } \n # Modulation: { cfg_modulation } { cfg_identity_str } { cfg_netname_str } { cfg_netkey_str } "
2026-01-02 17:16:24 +01:00
discovery_hash_material = info [ " transport_id " ] + info [ " name " ]
2025-12-31 13:50:49 +01:00
info [ " discovery_hash " ] = RNS . Identity . full_hash ( discovery_hash_material . encode ( " utf-8 " ) )
2025-12-31 01:07:08 +01:00
2026-08-16 00:29:14 +02:00
if info and OP_ADDR in unpacked :
if type ( unpacked [ OP_ADDR ] ) not in [ type ( None ) , bytes ] : raise ValueError ( " Invalid data in operator LXMF address field of announce " )
if unpacked [ OP_ADDR ] and len ( unpacked [ OP_ADDR ] ) == RNS . Identity . TRUNCATED_HASHLENGTH / / 8 :
info [ " operator_lxmf_address " ] = RNS . hexrep ( unpacked [ OP_ADDR ] , delimit = False )
2025-12-31 01:07:08 +01:00
if self . callback and callable ( self . callback ) : self . callback ( info )
except Exception as e :
2026-01-17 17:09:01 +01:00
RNS . log ( f " An error occurred while trying to decode discovered interface. The contained exception was: { e } " , RNS . LOG_DEBUG )
2025-12-31 13:50:49 +01:00
class InterfaceDiscovery ( ) :
2026-04-12 19:57:34 +02:00
THRESHOLD_UNKNOWN = 24 * 60 * 60
THRESHOLD_STALE = 3 * 24 * 60 * 60
THRESHOLD_REMOVE = 7 * 24 * 60 * 60
2025-12-31 14:51:56 +01:00
2026-04-12 19:57:34 +02:00
MONITOR_INTERVAL = 5
DETACH_THRESHOLD = 12
2026-01-03 00:36:50 +01:00
2026-04-12 19:57:34 +02:00
STATUS_STALE = 0
STATUS_UNKNOWN = 100
STATUS_AVAILABLE = 1000
STATUS_CODE_MAP = { " available " : STATUS_AVAILABLE , " unknown " : STATUS_UNKNOWN , " stale " : STATUS_STALE }
AUTOCONNECT_TYPES = [ " BackboneInterface " , " TCPServerInterface " ]
DISCOVERABLE_TYPES = [ " BackboneInterface " , " TCPServerInterface " , " I2PInterface " , " RNodeInterface " , " WeaveInterface " , " KISSInterface " ]
2025-12-31 13:50:49 +01:00
2026-07-23 12:52:35 +02:00
AC_TRANSPORT_MODE = RNS . Interfaces . Interface . Interface . MODE_GATEWAY
AC_GRAVITY = 0
2026-05-10 00:29:48 +02:00
discovery_lock = Lock ( )
2025-12-31 13:50:49 +01:00
def __init__ ( self , required_value = InterfaceAnnouncer . DEFAULT_STAMP_VALUE , callback = None , discover_interfaces = True ) :
2025-12-31 15:23:43 +01:00
if not required_value : required_value = InterfaceAnnouncer . DEFAULT_STAMP_VALUE
2026-01-03 00:36:50 +01:00
self . required_value = required_value
self . discovery_callback = callback
self . rns_instance = RNS . Reticulum . get_instance ( )
self . monitored_interfaces = [ ]
self . monitoring_autoconnects = False
self . monitor_interval = self . MONITOR_INTERVAL
self . detach_threshold = self . DETACH_THRESHOLD
2026-01-17 18:47:08 +01:00
self . initial_autoconnect_ran = False
2026-07-25 21:00:10 +02:00
self . blackholed_updated = 0
self . __blackholed = { }
2025-12-31 14:51:56 +01:00
if not self . rns_instance : raise SystemError ( " Attempt to start interface discovery listener without an active RNS instance " )
self . storagepath = os . path . join ( RNS . Reticulum . storagepath , " discovery " , " interfaces " )
if not os . path . isdir ( self . storagepath ) : os . makedirs ( self . storagepath )
2025-12-31 13:50:49 +01:00
if discover_interfaces :
2025-12-31 14:51:56 +01:00
self . handler = InterfaceAnnounceHandler ( callback = self . interface_discovered , required_value = self . required_value )
2025-12-31 13:50:49 +01:00
RNS . Transport . register_announce_handler ( self . handler )
2026-01-03 00:52:39 +01:00
threading . Thread ( target = self . connect_discovered , daemon = True ) . start ( )
2025-12-31 13:50:49 +01:00
2026-07-25 21:00:10 +02:00
def __blackholed_identities ( self ) :
st = time . time ( )
if not self . __blackholed or time . time ( ) > self . blackholed_updated + 60 :
self . __blackholed = self . rns_instance . get_blackholed_identities ( )
self . blackholed_updated = time . time ( )
return self . __blackholed
2026-01-17 18:47:08 +01:00
def list_discovered_interfaces ( self , only_available = False , only_transport = False ) :
2025-12-31 14:51:56 +01:00
now = time . time ( )
discovered_interfaces = [ ]
2026-01-02 21:03:18 +01:00
discovery_sources = RNS . Reticulum . interface_discovery_sources ( )
2026-07-25 21:00:10 +02:00
blackholed_identities = self . __blackholed_identities ( )
2025-12-31 14:51:56 +01:00
for filename in os . listdir ( self . storagepath ) :
try :
2026-05-10 00:29:48 +02:00
with self . discovery_lock :
filepath = os . path . join ( self . storagepath , filename )
with open ( filepath , " rb " ) as f : info = msgpack . unpackb ( f . read ( ) )
2026-01-02 21:03:18 +01:00
should_remove = False
2026-07-24 19:47:44 +02:00
heard_delta = now - info [ " last_heard " ]
info [ " name " ] = InterfaceAnnounceHandler . sanitize_name ( info [ " name " ] )
2026-01-02 21:03:18 +01:00
2026-07-24 19:47:44 +02:00
if heard_delta > self . THRESHOLD_REMOVE : should_remove = True
elif not " transport_id " in info or not info [ " transport_id " ] : should_remove = True
elif not " network_id " in info or not info [ " network_id " ] : should_remove = True
2026-01-02 21:03:18 +01:00
elif discovery_sources and not " network_id " in info : should_remove = True
elif discovery_sources and not bytes . fromhex ( info [ " network_id " ] ) in discovery_sources : should_remove = True
2026-04-12 19:57:34 +02:00
elif not " type " in info or ( " type " in info and not info [ " type " ] in self . DISCOVERABLE_TYPES ) : should_remove = True
2026-07-25 21:00:10 +02:00
elif bytes . fromhex ( info [ " network_id " ] ) in blackholed_identities : should_remove = True
elif bytes . fromhex ( info [ " transport_id " ] ) in blackholed_identities : should_remove = True
2026-01-17 17:17:39 +01:00
elif " reachable_on " in info :
if not ( is_ip_address ( info [ " reachable_on " ] ) or is_hostname ( info [ " reachable_on " ] ) ) : should_remove = True
2026-01-02 21:03:18 +01:00
if should_remove :
2025-12-31 14:51:56 +01:00
os . unlink ( filepath )
continue
else :
if heard_delta > self . THRESHOLD_STALE : info [ " status " ] = " stale "
elif heard_delta > self . THRESHOLD_UNKNOWN : info [ " status " ] = " unknown "
else : info [ " status " ] = " available "
info [ " status_code " ] = self . STATUS_CODE_MAP [ info [ " status " ] ]
2026-01-17 18:47:08 +01:00
if not only_available and not only_transport : discovered_interfaces . append ( info )
else :
should_append = True
status = info [ " status " ]
transport = info [ " transport " ]
if only_available and status != " available " : should_append = False
if only_transport and not transport : should_append = False
if should_append : discovered_interfaces . append ( info )
2025-12-31 14:51:56 +01:00
except Exception as e :
2026-05-10 00:29:48 +02:00
RNS . log ( f " Error while loading discovered interface data: { e } " , RNS . LOG_WARNING )
RNS . log ( f " The interface data file { os . path . join ( self . storagepath , filename ) } may be corrupt " , RNS . LOG_WARNING )
2025-12-31 14:51:56 +01:00
RNS . trace_exception ( e )
discovered_interfaces . sort ( key = lambda info : ( info [ " status_code " ] , info [ " value " ] , info [ " last_heard " ] ) , reverse = True )
return discovered_interfaces
2025-12-31 13:50:49 +01:00
def interface_discovered ( self , info ) :
try :
name = info [ " name " ]
2026-01-03 00:36:50 +01:00
value = info [ " value " ]
interface_type = info [ " type " ]
2025-12-31 13:50:49 +01:00
discovery_hash = info [ " discovery_hash " ]
2026-04-12 19:57:34 +02:00
discovered_type = info [ " type " ]
if not discovered_type in self . DISCOVERABLE_TYPES : return
2025-12-31 13:50:49 +01:00
hops = info [ " hops " ] ; ms = " " if hops == 1 else " s "
filename = RNS . hexrep ( discovery_hash , delimit = False )
filepath = os . path . join ( self . storagepath , filename )
2026-01-03 00:36:50 +01:00
RNS . log ( f " Discovered { interface_type } { hops } hop { ms } away with stamp value { value } : { name } " , RNS . LOG_DEBUG )
2026-05-10 00:29:48 +02:00
with self . discovery_lock :
if not os . path . isfile ( filepath ) :
try :
with open ( filepath , " wb " ) as f :
info [ " discovered " ] = info [ " received " ]
info [ " last_heard " ] = info [ " received " ]
info [ " heard_count " ] = 0
f . write ( msgpack . packb ( info ) )
except Exception as e :
RNS . log ( f " Error while persisting discovered interface data: { e } " , RNS . LOG_ERROR )
RNS . trace_exception ( e )
return
2025-12-31 13:50:49 +01:00
2026-05-10 00:29:48 +02:00
else :
discovered = None
heard_count = None
2026-05-09 00:26:42 +02:00
try :
2026-05-10 00:29:48 +02:00
try :
with open ( filepath , " rb " ) as f :
last_info = msgpack . unpackb ( f . read ( ) )
discovered = last_info [ " discovered " ]
heard_count = last_info [ " heard_count " ]
2026-05-09 00:26:42 +02:00
2026-05-10 00:29:48 +02:00
except Exception as e : RNS . log ( f " Error while reading existing data for discovered interface, re-creating data " , RNS . LOG_ERROR )
2025-12-31 13:50:49 +01:00
2026-05-10 00:29:48 +02:00
if discovered == None : discovered = info [ " received " ]
if heard_count == None : heard_count = 0
2025-12-31 13:50:49 +01:00
2026-05-10 00:29:48 +02:00
with open ( filepath , " wb " ) as f :
info [ " discovered " ] = discovered
info [ " last_heard " ] = info [ " received " ]
info [ " heard_count " ] = heard_count + 1
f . write ( msgpack . packb ( info ) )
2025-12-31 13:50:49 +01:00
2026-05-10 00:29:48 +02:00
except Exception as e :
RNS . log ( f " Error while persisting discovered interface data: { e } " , RNS . LOG_ERROR )
RNS . trace_exception ( e )
return
2025-12-31 13:50:49 +01:00
except Exception as e :
RNS . log ( f " Error processing discovered interface data: { e } " , RNS . LOG_ERROR )
RNS . trace_exception ( e )
return
2026-01-03 00:36:50 +01:00
self . autoconnect ( info )
try :
if self . discovery_callback and callable ( self . discovery_callback ) : self . discovery_callback ( info )
except Exception as e : RNS . log ( f " Error while processing external interface discovery callback: { e } " , RNS . LOG_ERROR )
def monitor_interface ( self , interface ) :
if not interface in self . monitored_interfaces :
self . monitored_interfaces . append ( interface )
if not self . monitoring_autoconnects :
self . monitoring_autoconnects = True
threading . Thread ( target = self . __monitor_job , daemon = True ) . start ( )
def __monitor_job ( self ) :
2026-05-05 23:42:00 +02:00
while self . monitoring_autoconnects and RNS . Transport . _should_run :
2026-01-03 00:36:50 +01:00
time . sleep ( self . monitor_interval )
detached_interfaces = [ ]
2026-01-03 20:00:22 +01:00
online_interfaces = 0
2026-01-17 18:47:08 +01:00
autoconnected_interfaces = self . autoconnect_count ( )
2026-01-03 00:36:50 +01:00
for interface in self . monitored_interfaces :
try :
if interface . online :
2026-01-03 20:00:22 +01:00
online_interfaces + = 1
2026-01-03 00:36:50 +01:00
if hasattr ( interface , " autoconnect_down " ) and interface . autoconnect_down != None :
RNS . log ( f " Auto-discovered interface { interface } reconnected " )
interface . autoconnect_down = None
else :
if not hasattr ( interface , " autoconnect_down " ) or interface . autoconnect_down == None :
RNS . log ( f " Auto-discovered interface { interface } disconnected " , RNS . LOG_DEBUG )
interface . autoconnect_down = time . time ( )
else :
down_for = time . time ( ) - interface . autoconnect_down
if down_for > = self . detach_threshold :
RNS . log ( f " Auto-discovered interface { interface } has been down for { RNS . prettytime ( down_for ) } , detaching " , RNS . LOG_DEBUG )
detached_interfaces . append ( interface )
except Exception as e :
RNS . log ( f " Error while checking auto-connected interface state for { interface } : { e } " , RNS . LOG_ERROR )
2026-01-17 18:47:08 +01:00
max_autoconnected_interfaces = RNS . Reticulum . max_autoconnected_interfaces ( )
free_slots = max ( 0 , max_autoconnected_interfaces - autoconnected_interfaces )
reserved_slots = max_autoconnected_interfaces / / 4
if online_interfaces > = max_autoconnected_interfaces :
2026-01-03 20:00:22 +01:00
for interface in RNS . Transport . interfaces :
if hasattr ( interface , " bootstrap_only " ) and interface . bootstrap_only == True :
RNS . log ( f " Tearing down bootstrap-only { interface } since target connected auto-discovered interface count has been reached " , RNS . LOG_INFO )
if not interface in detached_interfaces : detached_interfaces . append ( interface )
if online_interfaces == 0 :
2026-01-03 22:29:26 +01:00
if self . bootstrap_interface_count ( ) == 0 :
RNS . log ( f " No auto-discovered interfaces connected, re-enabling bootstrap interfaces " , RNS . LOG_NOTICE )
for config in RNS . Reticulum . get_instance ( ) . bootstrap_configs :
RNS . Reticulum . get_instance ( ) . _synthesize_interface ( config , config [ " name " ] )
2026-01-03 20:00:22 +01:00
2026-01-17 18:47:08 +01:00
if self . initial_autoconnect_ran and free_slots > reserved_slots :
candidate_interfaces = self . list_discovered_interfaces ( only_available = True , only_transport = True )
if len ( candidate_interfaces ) > 0 :
random . shuffle ( candidate_interfaces )
selected_interface = candidate_interfaces [ 0 ]
if not self . interface_exists ( selected_interface ) : self . autoconnect ( selected_interface )
2026-01-03 00:36:50 +01:00
for interface in detached_interfaces :
2026-01-03 20:00:22 +01:00
try : self . teardown_interface ( interface )
2026-01-03 00:36:50 +01:00
except Exception as e :
RNS . log ( f " Error while de-registering auto-connected interface from transport: { e } " , RNS . LOG_ERROR )
2026-01-03 20:00:22 +01:00
def teardown_interface ( self , interface ) :
interface . detach ( )
2026-05-15 17:08:22 +02:00
RNS . Transport . remove_interface ( interface )
2026-01-03 22:29:26 +01:00
if interface in self . monitored_interfaces : self . monitored_interfaces . remove ( interface )
2026-01-03 20:00:22 +01:00
2026-01-03 00:52:39 +01:00
def autoconnect_count ( self ) :
return len ( [ i for i in RNS . Transport . interfaces if hasattr ( i , " autoconnect_hash " ) ] )
2026-01-03 22:29:26 +01:00
def bootstrap_interface_count ( self ) :
return len ( [ i for i in RNS . Transport . interfaces if hasattr ( i , " bootstrap_only " ) and i . bootstrap_only == True ] )
2026-01-03 00:36:50 +01:00
2026-01-03 00:52:39 +01:00
def connect_discovered ( self ) :
if RNS . Reticulum . should_autoconnect_discovered_interfaces ( ) :
try :
2026-01-17 18:47:08 +01:00
discovered_interfaces = self . list_discovered_interfaces ( only_transport = True )
2026-01-03 00:52:39 +01:00
for info in discovered_interfaces :
if self . autoconnect_count ( ) > = RNS . Reticulum . max_autoconnected_interfaces ( ) : break
self . autoconnect ( info )
2026-01-17 18:47:08 +01:00
self . initial_autoconnect_ran = True
2026-01-03 00:52:39 +01:00
except Exception as e :
RNS . log ( f " Error while reconnecting discovered interfaces: { e } " , RNS . LOG_ERROR )
2026-01-17 18:47:08 +01:00
def endpoint_hash ( self , info ) :
endpoint_specifier = " "
if " reachable_on " in info : endpoint_specifier + = str ( info [ " reachable_on " ] )
if " port " in info : endpoint_specifier + = " : " + str ( info [ " port " ] )
endpoint_hash = RNS . Identity . full_hash ( endpoint_specifier . encode ( " utf-8 " ) )
return endpoint_hash
def interface_exists ( self , info ) :
exists = False
for interface in RNS . Transport . interfaces :
if hasattr ( interface , " autoconnect_hash " ) and interface . autoconnect_hash == self . endpoint_hash ( info ) :
exists = True
break
else :
dest_match = " reachable_on " in info and hasattr ( interface , " target_ip " ) and interface . target_ip == info [ " reachable_on " ]
port_match = not " port " in info or ( hasattr ( interface , " target_port " ) and " port " in info and interface . target_port == info [ " port " ] )
b32d_match = " reachable_on " in info and hasattr ( interface , " b32 " ) and interface . b32 == info [ " reachable_on " ]
if ( dest_match and port_match ) or b32d_match :
exists = True
break
return exists
2026-01-03 00:36:50 +01:00
def autoconnect ( self , info ) :
try :
if RNS . Reticulum . should_autoconnect_discovered_interfaces ( ) :
2026-01-03 00:52:39 +01:00
autoconnected_count = self . autoconnect_count ( )
2026-01-03 00:36:50 +01:00
if autoconnected_count < RNS . Reticulum . max_autoconnected_interfaces ( ) :
interface_type = info [ " type " ]
if interface_type in self . AUTOCONNECT_TYPES :
2026-01-17 18:47:08 +01:00
endpoint_hash = self . endpoint_hash ( info )
exists = self . interface_exists ( info )
2026-01-03 00:36:50 +01:00
if exists : RNS . log ( f " Discovered { interface_type } already exists, not auto-connecting " , RNS . LOG_DEBUG )
else :
if interface_type == " TCPClientInterface " :
RNS . log ( f " Your operating system does not support the Backbone interface type, and must degrade to using TCPClientInterface instead " , RNS . LOG_WARNING )
RNS . log ( f " Auto-connecting discovered TCPClient interfaces is not yet implemented, aborting auto-connect " , RNS . LOG_WARNING )
RNS . log ( f " You can obtain the configuration entry and add this interface manually instead using rnstatus -D " , RNS . LOG_WARNING )
return
if interface_type == " I2PInterface " :
2026-08-16 00:29:14 +02:00
RNS . log ( f " Auto-connecting discovered I2P interfaces is not yet implemented, aborting auto-connect " , RNS . LOG_DEBUG )
RNS . log ( f " You can obtain the configuration entry and add this interface manually instead using rnstatus -D " , RNS . LOG_DEBUG )
2026-01-03 00:36:50 +01:00
return
2026-05-06 04:57:20 +02:00
if is_ygg_ipv6 ( info [ " reachable_on " ] ) :
# TODO: Somehow detect if yggdrasil is enabled on the system
return
2026-08-16 00:29:14 +02:00
if is_onion_address ( info [ " reachable_on " ] ) :
# TODO: Somehow detect if TOR is enabled on the system
return
if is_ip_address ( info [ " reachable_on " ] ) and is_invalid_ip_address ( info [ " reachable_on " ] ) :
2026-08-17 21:52:53 +02:00
RNS . log ( f " Not auto-connecting discovered interface with invalid IP address: { info [ ' reachable_on ' ] } " , RNS . LOG_DEBUG )
2026-08-16 00:29:14 +02:00
return
2026-01-03 22:29:26 +01:00
interface_name = info [ " name " ]
2026-01-03 00:36:50 +01:00
config_entry = info [ " config_entry " ]
interface_config = { }
interface_config [ " name " ] = f " { interface_name } "
ifac_netname = info [ " ifac_netname " ] if " ifac_netname " in info else None
ifac_netkey = info [ " ifac_netkey " ] if " ifac_netkey " in info else None
interface = None
if interface_type == " BackboneInterface " :
from RNS . Interfaces import BackboneInterface
interface_config [ " target_host " ] = info [ " reachable_on " ]
interface_config [ " target_port " ] = info [ " port " ]
interface = BackboneInterface . BackboneClientInterface ( RNS . Transport , interface_config )
if interface :
2026-05-06 04:46:23 +02:00
RNS . log ( f " Auto-connecting discovered { interface_type } { interface_name } " )
2026-01-03 00:36:50 +01:00
interface . autoconnect_hash = endpoint_hash
interface . autoconnect_source = info [ " network_id " ]
2026-07-22 19:45:21 +02:00
if RNS . Reticulum . autoconnect_interface_mode ( ) : mode = RNS . Reticulum . autoconnect_interface_mode ( )
2026-07-23 12:52:35 +02:00
else : mode = self . AC_TRANSPORT_MODE if RNS . Reticulum . transport_enabled ( ) else None
2026-07-22 19:45:21 +02:00
internal_a = True if RNS . Reticulum . autoconnect_announces_to_internal ( ) else None
2026-07-23 12:52:35 +02:00
gravity = RNS . Reticulum . autoconnect_interface_gravity ( ) or self . AC_GRAVITY
2026-05-06 03:29:40 +02:00
ar_target = RNS . Reticulum . get_instance ( ) . _default_ar_target ( ) if RNS . Reticulum . transport_enabled ( ) else None
ar_penalty = RNS . Reticulum . get_instance ( ) . _default_ar_penalty ( ) if RNS . Reticulum . transport_enabled ( ) else None
ar_grace = RNS . Reticulum . get_instance ( ) . _default_ar_grace ( ) if RNS . Reticulum . transport_enabled ( ) else None
2026-05-09 04:43:22 +02:00
RNS . Reticulum . get_instance ( ) . _add_interface ( interface , mode = mode , ifac_netname = ifac_netname , ifac_netkey = ifac_netkey , configured_bitrate = 5E6 ,
2026-07-22 19:45:21 +02:00
announce_rate_target = ar_target , announce_rate_grace = ar_grace , announce_rate_penalty = ar_penalty ,
2026-07-23 12:52:35 +02:00
announces_to_internal = internal_a , gravity = gravity )
2026-01-03 00:36:50 +01:00
self . monitor_interface ( interface )
except Exception as e :
RNS . log ( f " Error while auto-connecting discovered interface: { e } " , RNS . LOG_ERROR )
RNS . trace_exception ( e )
2026-01-01 23:12:40 +01:00
class BlackholeUpdater ( ) :
INITIAL_WAIT = 20
JOB_INTERVAL = 60
2026-01-02 13:34:05 +01:00
UPDATE_INTERVAL = 1 * 60 * 60
2026-01-01 23:12:40 +01:00
SOURCE_TIMEOUT = 25
def __init__ ( self ) :
self . last_updates = { }
self . should_run = False
self . job_interval = self . JOB_INTERVAL
self . update_lock = threading . Lock ( )
def start ( self ) :
if not self . should_run :
source_count = len ( RNS . Reticulum . blackhole_sources ( ) )
ms = " " if source_count == 1 else " s "
RNS . log ( f " Starting blackhole updater with { source_count } source { ms } " , RNS . LOG_DEBUG )
self . should_run = True
threading . Thread ( target = self . job , daemon = True ) . start ( )
def stop ( self ) : self . should_run = False
def update_link_established ( self , link ) :
remote_identity = link . get_remote_identity ( )
RNS . log ( f " Link established for blackhole list update from { RNS . prettyhexrep ( remote_identity . hash ) } " , RNS . LOG_DEBUG )
receipt = link . request ( " /list " )
while not receipt . concluded ( ) : time . sleep ( 0.2 )
response = receipt . get_response ( )
link . teardown ( )
if type ( response ) == dict : blackhole_list = response
else : blackhole_list = None
if blackhole_list :
added = 0
for identity_hash in blackhole_list :
entry = blackhole_list [ identity_hash ]
if not identity_hash in RNS . Transport . blackholed_identities :
RNS . Transport . blackholed_identities [ identity_hash ] = entry
added + = 1
if added > 0 :
spec = " identity " if added == 1 else " identities "
RNS . log ( f " Added { added } blackholed { spec } from { RNS . prettyhexrep ( remote_identity . hash ) } " , RNS . LOG_DEBUG )
try :
sourcelistpath = os . path . join ( RNS . Reticulum . blackholepath , RNS . hexrep ( remote_identity . hash , delimit = False ) )
tmppath = f " { sourcelistpath } .tmp "
with open ( tmppath , " wb " ) as f : f . write ( msgpack . packb ( blackhole_list ) )
if os . path . isfile ( sourcelistpath ) : os . unlink ( sourcelistpath )
os . rename ( tmppath , sourcelistpath )
except Exception as e :
RNS . log ( f " Error while persisting blackhole list from { RNS . prettyhexrep ( remote_identity . hash ) } : { e } " , RNS . LOG_ERROR )
RNS . log ( f " Blackhole list update from { RNS . prettyhexrep ( remote_identity . hash ) } completed " , RNS . LOG_DEBUG )
def job ( self ) :
time . sleep ( self . INITIAL_WAIT )
while self . should_run :
try :
now = time . time ( )
for identity_hash in RNS . Reticulum . blackhole_sources ( ) :
if identity_hash in self . last_updates : last_update = self . last_updates [ identity_hash ]
else : last_update = 0
2026-05-28 17:12:21 +02:00
if now > last_update + RNS . Reticulum . blackhole_update_interval ( ) :
2026-01-01 23:12:40 +01:00
try :
destination_hash = RNS . Destination . hash_from_name_and_identity ( " rnstransport.info.blackhole " , identity_hash )
RNS . log ( f " Attempting blackhole list update from { RNS . prettyhexrep ( identity_hash ) } ... " , RNS . LOG_DEBUG )
if not RNS . Transport . await_path ( destination_hash ) : RNS . log ( f " No path available for blackhole list update from { RNS . prettyhexrep ( identity_hash ) } , retrying later " , RNS . LOG_VERBOSE )
else :
remote_identity = RNS . Identity . recall ( destination_hash )
destination = RNS . Destination ( remote_identity , RNS . Destination . OUT , RNS . Destination . SINGLE , " rnstransport " , " info " , " blackhole " )
RNS . Link ( destination , established_callback = self . update_link_established )
self . last_updates [ identity_hash ] = time . time ( )
except Exception as e :
RNS . log ( f " Error while establishing link for blackhole list update from { RNS . prettyhexrep ( identity_hash ) } : { e } " , RNS . LOG_ERROR )
except Exception as e :
RNS . log ( f " Error in blackhole list updater job: { e } " , RNS . LOG_ERROR )
RNS . trace_exception ( e )
2026-01-17 17:09:01 +01:00
time . sleep ( self . job_interval )
def is_ip_address ( address_string ) :
try :
ipaddress . ip_address ( address_string )
return True
except : return False
2026-05-06 04:57:20 +02:00
def is_ygg_ipv6 ( address_string ) :
try : return ipaddress . ip_address ( address_string ) in ipaddress . IPv6Network ( " 200::/7 " )
except : return False
2026-08-16 00:29:14 +02:00
def is_onion_address ( address_string ) :
try : return address_string . lower ( ) . endswith ( " .onion " )
except : return False
INVALID_IP_ADDRESSES = [ " 127.0.0.1 " , " 0.0.0.0 " ]
def is_invalid_ip_address ( address_string ) :
if not address_string in INVALID_IP_ADDRESSES : return False
else : return True
2026-01-17 17:09:01 +01:00
def is_hostname ( hostname ) :
if hostname [ - 1 ] == " . " : hostname = hostname [ : - 1 ]
if len ( hostname ) > 253 : return False
components = hostname . split ( " . " )
if re . match ( r " [0-9]+$ " , components [ - 1 ] ) : return False
allowed = re . compile ( r " (?!-)[a-z0-9-] { 1,63}(?<!-)$ " , re . IGNORECASE )
return all ( allowed . match ( label ) for label in components )
2026-05-06 04:24:07 +02:00
san_map = " "
for i in range ( 48 , 58 ) : san_map + = bytes ( [ i ] ) . decode ( " ascii " )
for i in range ( 65 , 91 ) : san_map + = bytes ( [ i ] ) . decode ( " ascii " )
for i in range ( 97 , 123 ) : san_map + = bytes ( [ i ] ) . decode ( " ascii " )