2022-06-17 08:54:04 +02:00
import os
2025-01-21 16:33:39 +01:00
import sys
2022-06-17 08:54:04 +02:00
import time
2025-01-21 10:46:59 +01:00
import math
2022-06-17 08:54:04 +02:00
import random
2022-11-19 20:07:00 +01:00
import base64
2022-06-17 08:54:04 +02:00
import atexit
2025-01-21 16:33:39 +01:00
import signal
2022-06-17 08:54:04 +02:00
import threading
2025-01-21 20:20:39 +01:00
from collections import deque
2022-06-17 08:54:04 +02:00
import RNS
import RNS . vendor . umsgpack as msgpack
from . LXMF import APP_NAME
2024-09-07 15:17:48 +02:00
from . LXMF import FIELD_TICKET
2025-10-31 22:24:55 +01:00
from . LXMF import PN_META_NAME
2025-10-30 21:19:38 +01:00
from . LXMF import pn_announce_data_is_valid
2022-06-17 08:54:04 +02:00
from . LXMPeer import LXMPeer
from . LXMessage import LXMessage
from . Handlers import LXMFDeliveryAnnounceHandler
from . Handlers import LXMFPropagationAnnounceHandler
2025-01-18 20:11:31 +01:00
import LXMF . LXStamper as LXStamper
2022-06-17 08:54:04 +02:00
class LXMRouter :
2022-12-23 23:49:01 +01:00
MAX_DELIVERY_ATTEMPTS = 5
2022-12-22 21:23:29 +01:00
PROCESSING_INTERVAL = 4
2024-03-17 00:35:45 +01:00
DELIVERY_RETRY_WAIT = 10
2023-11-02 04:36:51 +01:00
PATH_REQUEST_WAIT = 7
2024-09-17 12:56:56 +02:00
MAX_PATHLESS_TRIES = 1
2022-06-17 08:54:04 +02:00
LINK_MAX_INACTIVITY = 10 * 60
2024-02-29 23:02:16 +01:00
P_LINK_MAX_INACTIVITY = 3 * 60
2022-06-17 08:54:04 +02:00
MESSAGE_EXPIRY = 30 * 24 * 60 * 60
2024-09-06 20:34:09 +02:00
STAMP_COST_EXPIRY = 45 * 24 * 60 * 60
2022-06-17 08:54:04 +02:00
NODE_ANNOUNCE_DELAY = 20
2025-11-01 17:32:45 +01:00
MAX_PEERS = 20
2022-06-17 08:54:04 +02:00
AUTOPEER = True
AUTOPEER_MAXDEPTH = 4
2023-02-17 17:10:31 +01:00
FASTEST_N_RANDOM_POOL = 2
2025-01-29 01:26:36 +01:00
ROTATION_HEADROOM_PCT = 10
2025-01-29 21:21:51 +01:00
ROTATION_AR_MAX = 0.5
2022-06-17 08:54:04 +02:00
2025-10-31 17:02:34 +01:00
PEERING_COST = 18
MAX_PEERING_COST = 24
2025-10-31 13:53:59 +01:00
PROPAGATION_COST_MIN = 13
2025-10-30 14:08:39 +01:00
PROPAGATION_COST_FLEX = 3
2025-10-31 13:53:59 +01:00
PROPAGATION_COST = 16
2024-03-01 22:37:54 +01:00
PROPAGATION_LIMIT = 256
2025-10-29 23:24:45 +01:00
SYNC_LIMIT = PROPAGATION_LIMIT * 40
2024-03-01 23:26:27 +01:00
DELIVERY_LIMIT = 1000
2024-03-01 22:37:54 +01:00
PR_PATH_TIMEOUT = 10
2025-11-01 01:35:15 +01:00
PN_STAMP_THROTTLE = 180
2024-03-01 22:37:54 +01:00
PR_IDLE = 0x00
PR_PATH_REQUESTED = 0x01
PR_LINK_ESTABLISHING = 0x02
PR_LINK_ESTABLISHED = 0x03
PR_REQUEST_SENT = 0x04
PR_RECEIVING = 0x05
PR_RESPONSE_RECEIVED = 0x06
PR_COMPLETE = 0x07
PR_NO_PATH = 0xf0
PR_LINK_FAILED = 0xf1
PR_TRANSFER_FAILED = 0xf2
PR_NO_IDENTITY_RCVD = 0xf3
PR_NO_ACCESS = 0xf4
PR_FAILED = 0xfe
PR_ALL_MESSAGES = 0x00
2022-06-17 08:54:04 +02:00
2025-11-02 02:27:57 +01:00
DUPLICATE_SIGNAL = " lxmf_duplicate "
2025-01-23 14:13:08 +01:00
STATS_GET_PATH = " /pn/get/stats "
2025-11-01 00:10:30 +01:00
SYNC_REQUEST_PATH = " /pn/peer/sync "
2025-11-01 17:32:45 +01:00
UNPEER_REQUEST_PATH = " /pn/peer/unpeer "
2025-01-23 14:13:08 +01:00
2022-06-17 09:15:56 +02:00
### Developer-facing API ##############################
#######################################################
2025-01-22 01:37:09 +01:00
def __init__ ( self , identity = None , storagepath = None , autopeer = AUTOPEER , autopeer_maxdepth = None ,
2025-10-29 23:24:45 +01:00
propagation_limit = PROPAGATION_LIMIT , delivery_limit = DELIVERY_LIMIT , sync_limit = SYNC_LIMIT ,
enforce_ratchets = False , enforce_stamps = False , static_peers = [ ] , max_peers = None ,
2025-10-30 14:08:39 +01:00
from_static_only = False , sync_strategy = LXMPeer . STRATEGY_PERSISTENT ,
2025-10-31 13:53:59 +01:00
propagation_cost = PROPAGATION_COST , propagation_cost_flexibility = PROPAGATION_COST_FLEX ,
2025-11-01 18:27:11 +01:00
peering_cost = PEERING_COST , max_peering_cost = MAX_PEERING_COST , name = None ) :
2025-01-22 01:37:09 +01:00
2022-06-17 08:54:04 +02:00
random . seed ( os . urandom ( 10 ) )
self . pending_inbound = [ ]
self . pending_outbound = [ ]
self . failed_outbound = [ ]
self . direct_links = { }
2024-09-12 17:57:26 +02:00
self . backchannel_links = { }
2022-06-17 08:54:04 +02:00
self . delivery_destinations = { }
2022-06-17 14:07:40 +02:00
self . prioritised_list = [ ]
2022-06-17 08:54:04 +02:00
self . ignored_list = [ ]
2022-06-17 13:42:44 +02:00
self . allowed_list = [ ]
2025-11-01 13:10:28 +01:00
self . control_allowed_list = [ ]
2022-06-17 13:42:44 +02:00
self . auth_required = False
2024-03-02 09:09:51 +01:00
self . retain_synced_on_node = False
2022-06-17 08:54:04 +02:00
2025-10-29 23:24:45 +01:00
self . default_sync_strategy = sync_strategy
self . processing_outbound = False
self . processing_inbound = False
self . processing_count = 0
2025-10-31 22:24:55 +01:00
self . name = name
2022-06-17 08:54:04 +02:00
self . propagation_node = False
2025-10-31 22:24:55 +01:00
self . propagation_node_start_time = None
2022-06-17 08:54:04 +02:00
2025-10-30 14:08:39 +01:00
if storagepath == None : raise ValueError ( " LXMF cannot be initialised without a storage path " )
2022-06-17 08:54:04 +02:00
else :
self . storagepath = storagepath + " /lxmf "
2024-09-04 18:57:51 +02:00
self . ratchetpath = self . storagepath + " /ratchets "
2022-06-17 08:54:04 +02:00
self . outbound_propagation_node = None
self . outbound_propagation_link = None
2025-10-29 23:24:45 +01:00
if delivery_limit == None : delivery_limit = LXMRouter . DELIVERY_LIMIT
2025-10-31 13:53:59 +01:00
if propagation_cost < LXMRouter . PROPAGATION_COST_MIN : propagation_cost = LXMRouter . PROPAGATION_COST_MIN
self . message_storage_limit = None
self . information_storage_limit = None
self . propagation_per_transfer_limit = propagation_limit
self . propagation_per_sync_limit = sync_limit
self . delivery_per_transfer_limit = delivery_limit
self . propagation_stamp_cost = propagation_cost
2025-10-30 14:08:39 +01:00
self . propagation_stamp_cost_flexibility = propagation_cost_flexibility
2025-10-31 13:53:59 +01:00
self . peering_cost = peering_cost
2025-11-01 18:27:11 +01:00
self . max_peering_cost = max_peering_cost
2025-10-31 13:53:59 +01:00
self . enforce_ratchets = enforce_ratchets
self . _enforce_stamps = enforce_stamps
self . pending_deferred_stamps = { }
2025-11-01 01:35:15 +01:00
self . throttled_peers = { }
2022-06-17 12:37:00 +02:00
2025-10-29 23:24:45 +01:00
if sync_limit == None or self . propagation_per_sync_limit < self . propagation_per_transfer_limit :
self . propagation_per_sync_limit = self . propagation_per_transfer_limit
2022-06-17 08:54:04 +02:00
self . wants_download_on_path_available_from = None
self . wants_download_on_path_available_to = None
self . propagation_transfer_state = LXMRouter . PR_IDLE
self . propagation_transfer_progress = 0.0
self . propagation_transfer_last_result = None
2025-11-02 02:29:32 +01:00
self . propagation_transfer_last_duplicates = None
2022-06-17 08:54:04 +02:00
self . propagation_transfer_max_messages = None
2025-01-29 01:26:36 +01:00
self . prioritise_rotating_unreachable_peers = False
2024-02-29 23:02:16 +01:00
self . active_propagation_links = [ ]
2025-11-02 18:35:15 +01:00
self . validated_peer_links = { }
2022-06-17 08:54:04 +02:00
self . locally_delivered_transient_ids = { }
2022-11-19 20:07:00 +01:00
self . locally_processed_transient_ids = { }
2024-09-06 20:34:09 +02:00
self . outbound_stamp_costs = { }
2024-09-07 15:17:48 +02:00
self . available_tickets = { " outbound " : { } , " inbound " : { } , " last_deliveries " : { } }
2024-09-06 20:34:09 +02:00
self . cost_file_lock = threading . Lock ( )
2024-09-07 15:17:48 +02:00
self . ticket_file_lock = threading . Lock ( )
2024-09-07 22:40:13 +02:00
self . stamp_gen_lock = threading . Lock ( )
2025-01-21 16:33:39 +01:00
self . exit_handler_running = False
2022-06-17 08:54:04 +02:00
if identity == None :
identity = RNS . Identity ( )
self . identity = identity
self . propagation_destination = RNS . Destination ( self . identity , RNS . Destination . IN , RNS . Destination . SINGLE , APP_NAME , " propagation " )
2025-10-31 21:45:40 +01:00
self . propagation_destination . set_default_app_data ( self . get_propagation_node_app_data )
2025-01-23 14:13:08 +01:00
self . control_destination = None
self . client_propagation_messages_received = 0
self . client_propagation_messages_served = 0
self . unpeered_propagation_incoming = 0
self . unpeered_propagation_rx_bytes = 0
2022-06-17 08:54:04 +02:00
2025-10-30 14:08:39 +01:00
if autopeer != None : self . autopeer = autopeer
else : self . autopeer = LXMRouter . AUTOPEER
2022-10-22 22:12:14 +02:00
2025-10-30 14:08:39 +01:00
if autopeer_maxdepth != None : self . autopeer_maxdepth = autopeer_maxdepth
else : self . autopeer_maxdepth = LXMRouter . AUTOPEER_MAXDEPTH
2022-06-17 08:54:04 +02:00
2025-10-30 14:08:39 +01:00
if max_peers == None : self . max_peers = LXMRouter . MAX_PEERS
2025-01-22 01:37:09 +01:00
else :
2025-10-30 14:08:39 +01:00
if type ( max_peers ) == int and max_peers > = 0 : self . max_peers = max_peers
else : raise ValueError ( f " Invalid value for max_peers: { max_peers } " )
2025-01-22 01:37:09 +01:00
self . from_static_only = from_static_only
2025-10-30 14:08:39 +01:00
if type ( static_peers ) != list : raise ValueError ( f " Invalid type supplied for static peer list: { type ( static_peers ) } " )
2025-01-22 01:37:09 +01:00
else :
for static_peer in static_peers :
2025-10-30 14:08:39 +01:00
if type ( static_peer ) != bytes : raise ValueError ( f " Invalid static peer destination hash: { static_peer } " )
2025-01-22 01:37:09 +01:00
else :
2025-10-30 14:08:39 +01:00
if len ( static_peer ) != RNS . Reticulum . TRUNCATED_HASHLENGTH / / 8 : raise ValueError ( f " Invalid static peer destination hash: { static_peer } " )
2025-01-22 01:37:09 +01:00
self . static_peers = static_peers
2022-06-17 08:54:04 +02:00
self . peers = { }
self . propagation_entries = { }
2025-01-21 20:20:39 +01:00
self . peer_distribution_queue = deque ( )
2022-06-17 08:54:04 +02:00
RNS . Transport . register_announce_handler ( LXMFDeliveryAnnounceHandler ( self ) )
RNS . Transport . register_announce_handler ( LXMFPropagationAnnounceHandler ( self ) )
self . __delivery_callback = None
try :
if os . path . isfile ( self . storagepath + " /local_deliveries " ) :
locally_delivered_file = open ( self . storagepath + " /local_deliveries " , " rb " )
data = locally_delivered_file . read ( )
locally_delivered_file . close ( )
2024-12-23 12:36:53 +01:00
self . locally_delivered_transient_ids = msgpack . unpackb ( data )
2025-01-14 21:32:10 +01:00
if not type ( self . locally_delivered_transient_ids ) == dict :
RNS . log ( " Invalid data format for loaded locally delivered transient IDs, recreating... " , RNS . LOG_ERROR )
self . locally_delivered_transient_ids = { }
2022-11-19 20:07:00 +01:00
2024-12-23 12:36:53 +01:00
except Exception as e :
RNS . log ( " Could not load locally delivered message ID cache from storage. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
self . locally_delivered_transient_ids = { }
try :
2022-11-19 20:07:00 +01:00
if os . path . isfile ( self . storagepath + " /locally_processed " ) :
locally_processed_file = open ( self . storagepath + " /locally_processed " , " rb " )
data = locally_processed_file . read ( )
locally_processed_file . close ( )
2024-12-23 12:36:53 +01:00
self . locally_processed_transient_ids = msgpack . unpackb ( data )
2025-01-14 21:32:10 +01:00
if not type ( self . locally_processed_transient_ids ) == dict :
RNS . log ( " Invalid data format for loaded locally processed transient IDs, recreating... " , RNS . LOG_ERROR )
self . locally_processed_transient_ids = { }
2024-12-23 12:36:53 +01:00
except Exception as e :
RNS . log ( " Could not load locally processed message ID cache from storage. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
self . locally_processed_transient_ids = { }
try :
2022-11-19 20:07:00 +01:00
self . clean_transient_id_caches ( )
2022-06-17 08:54:04 +02:00
except Exception as e :
2024-12-23 12:36:53 +01:00
RNS . log ( " Could not clean transient ID caches. The contained exception was : " + str ( e ) , RNS . LOG_ERROR )
self . locally_delivered_transient_ids = { }
self . locally_processed_transient_ids = { }
2022-06-17 08:54:04 +02:00
2024-09-06 20:34:09 +02:00
try :
if os . path . isfile ( self . storagepath + " /outbound_stamp_costs " ) :
with self . cost_file_lock :
with open ( self . storagepath + " /outbound_stamp_costs " , " rb " ) as outbound_stamp_cost_file :
data = outbound_stamp_cost_file . read ( )
self . outbound_stamp_costs = msgpack . unpackb ( data )
2025-01-14 21:32:10 +01:00
if not type ( self . outbound_stamp_costs ) == dict :
RNS . log ( " Invalid data format for loaded outbound stamp costs, recreating... " , RNS . LOG_ERROR )
self . outbound_stamp_costs = { }
2024-09-06 20:34:09 +02:00
self . clean_outbound_stamp_costs ( )
self . save_outbound_stamp_costs ( )
except Exception as e :
RNS . log ( " Could not load outbound stamp costs from storage. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2024-09-07 15:17:48 +02:00
try :
if os . path . isfile ( self . storagepath + " /available_tickets " ) :
with self . ticket_file_lock :
with open ( self . storagepath + " /available_tickets " , " rb " ) as available_tickets_file :
data = available_tickets_file . read ( )
self . available_tickets = msgpack . unpackb ( data )
if not type ( self . available_tickets ) == dict :
RNS . log ( " Invalid data format for loaded available tickets, recreating... " , RNS . LOG_ERROR )
self . available_tickets = { " outbound " : { } , " inbound " : { } , " last_deliveries " : { } }
if not " outbound " in self . available_tickets :
RNS . log ( " Missing outbound entry in loaded available tickets, recreating... " , RNS . LOG_ERROR )
self . available_tickets [ " outbound " ] = { }
if not " inbound " in self . available_tickets :
RNS . log ( " Missing inbound entry in loaded available tickets, recreating... " , RNS . LOG_ERROR )
self . available_tickets [ " inbound " ] = { }
if not " last_deliveries " in self . available_tickets :
RNS . log ( " Missing local_deliveries entry in loaded available tickets, recreating... " , RNS . LOG_ERROR )
self . available_tickets [ " last_deliveries " ] = { }
self . clean_available_tickets ( )
self . save_available_tickets ( )
except Exception as e :
RNS . log ( " Could not load outbound stamp costs from storage. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2022-06-17 08:54:04 +02:00
atexit . register ( self . exit_handler )
2025-01-21 16:33:39 +01:00
signal . signal ( signal . SIGINT , self . sigint_handler )
signal . signal ( signal . SIGTERM , self . sigterm_handler )
2022-06-17 08:54:04 +02:00
job_thread = threading . Thread ( target = self . jobloop )
job_thread . setDaemon ( True )
job_thread . start ( )
2024-09-07 11:35:17 +02:00
def announce ( self , destination_hash , attached_interface = None ) :
2022-06-17 09:15:56 +02:00
if destination_hash in self . delivery_destinations :
2024-09-07 11:35:17 +02:00
self . delivery_destinations [ destination_hash ] . announce ( app_data = self . get_announce_app_data ( destination_hash ) , attached_interface = attached_interface )
2022-06-17 08:54:04 +02:00
2025-10-31 22:24:55 +01:00
def get_propagation_node_announce_metadata ( self ) :
metadata = { }
if self . name : metadata [ PN_META_NAME ] = str ( self . name ) . encode ( " utf-8 " )
return metadata
2025-10-31 21:45:40 +01:00
def get_propagation_node_app_data ( self ) :
2025-10-31 22:24:55 +01:00
metadata = self . get_propagation_node_announce_metadata ( )
2025-10-31 21:45:40 +01:00
node_state = self . propagation_node and not self . from_static_only
stamp_cost = [ self . propagation_stamp_cost , self . propagation_stamp_cost_flexibility , self . peering_cost ]
announce_data = [ False , # 0: Legacy LXMF PN support
int ( time . time ( ) ) , # 1: Current node timebase
node_state , # 2: Boolean flag signalling propagation node state
self . propagation_per_transfer_limit , # 3: Per-transfer limit for message propagation in kilobytes
self . propagation_per_sync_limit , # 4: Limit for incoming propagation node syncs
stamp_cost , # 5: Propagation stamp cost for this node
metadata ] # 6: Node metadata
return msgpack . packb ( announce_data )
2022-06-17 09:15:56 +02:00
def announce_propagation_node ( self ) :
def delayed_announce ( ) :
time . sleep ( LXMRouter . NODE_ANNOUNCE_DELAY )
2025-10-31 21:45:40 +01:00
self . propagation_destination . announce ( app_data = self . get_propagation_node_app_data ( ) )
2022-06-17 08:54:04 +02:00
2022-06-17 09:15:56 +02:00
da_thread = threading . Thread ( target = delayed_announce )
da_thread . setDaemon ( True )
da_thread . start ( )
2022-06-17 08:54:04 +02:00
2024-09-06 20:34:09 +02:00
def register_delivery_identity ( self , identity , display_name = None , stamp_cost = None ) :
2024-09-07 15:17:48 +02:00
if len ( self . delivery_destinations ) != 0 :
RNS . log ( " Currently only one delivery identity is supported per LXMF router instance " , RNS . LOG_ERROR )
return None
2024-09-04 18:57:51 +02:00
if not os . path . isdir ( self . ratchetpath ) :
os . makedirs ( self . ratchetpath )
2023-02-03 17:56:30 +01:00
delivery_destination = RNS . Destination ( identity , RNS . Destination . IN , RNS . Destination . SINGLE , APP_NAME , " delivery " )
2024-09-04 18:57:51 +02:00
delivery_destination . enable_ratchets ( f " { self . ratchetpath } / { RNS . hexrep ( delivery_destination . hash , delimit = False ) } .ratchets " )
2022-06-17 08:54:04 +02:00
delivery_destination . set_packet_callback ( self . delivery_packet )
delivery_destination . set_link_established_callback ( self . delivery_link_established )
delivery_destination . display_name = display_name
2024-09-06 20:34:09 +02:00
2024-09-05 14:56:49 +02:00
if self . enforce_ratchets :
delivery_destination . enforce_ratchets ( )
2022-06-17 08:54:04 +02:00
if display_name != None :
2024-09-06 20:34:09 +02:00
def get_app_data ( ) :
2024-09-07 22:40:13 +02:00
return self . get_announce_app_data ( delivery_destination . hash )
2024-09-06 20:34:09 +02:00
delivery_destination . set_default_app_data ( get_app_data )
2022-06-17 08:54:04 +02:00
self . delivery_destinations [ delivery_destination . hash ] = delivery_destination
2024-09-07 11:35:17 +02:00
self . set_inbound_stamp_cost ( delivery_destination . hash , stamp_cost )
2022-06-17 08:54:04 +02:00
return delivery_destination
def register_delivery_callback ( self , callback ) :
self . __delivery_callback = callback
2024-09-07 11:35:17 +02:00
def set_inbound_stamp_cost ( self , destination_hash , stamp_cost ) :
if destination_hash in self . delivery_destinations :
delivery_destination = self . delivery_destinations [ destination_hash ]
if stamp_cost == None :
delivery_destination . stamp_cost = None
return True
elif type ( stamp_cost ) == int :
if stamp_cost < 1 :
delivery_destination . stamp_cost = None
elif stamp_cost < 255 :
delivery_destination . stamp_cost = stamp_cost
else :
return False
return True
return False
def get_outbound_stamp_cost ( self , destination_hash ) :
if destination_hash in self . outbound_stamp_costs :
stamp_cost = self . outbound_stamp_costs [ destination_hash ] [ 1 ]
return stamp_cost
else :
return None
2024-09-17 13:18:16 +02:00
def set_active_propagation_node ( self , destination_hash ) :
self . set_outbound_propagation_node ( destination_hash )
# self.set_inbound_propagation_node(destination_hash)
2022-06-17 08:54:04 +02:00
def set_outbound_propagation_node ( self , destination_hash ) :
if len ( destination_hash ) != RNS . Identity . TRUNCATED_HASHLENGTH / / 8 or type ( destination_hash ) != bytes :
raise ValueError ( " Invalid destination hash for outbound propagation node " )
else :
2024-10-05 22:40:14 +02:00
if self . outbound_propagation_node != destination_hash :
self . outbound_propagation_node = destination_hash
if self . outbound_propagation_link != None :
if self . outbound_propagation_link . destination . hash != destination_hash :
self . outbound_propagation_link . teardown ( )
self . outbound_propagation_link = None
2022-06-17 08:54:04 +02:00
def get_outbound_propagation_node ( self ) :
return self . outbound_propagation_node
2025-10-31 21:45:40 +01:00
def get_outbound_propagation_cost ( self ) :
target_propagation_cost = None
pn_destination_hash = self . get_outbound_propagation_node ( )
pn_app_data = RNS . Identity . recall_app_data ( pn_destination_hash )
if pn_announce_data_is_valid ( pn_app_data ) :
pn_config = msgpack . unpackb ( pn_app_data )
target_propagation_cost = pn_config [ 5 ] [ 0 ]
if not target_propagation_cost :
RNS . log ( f " Could not retrieve cached propagation node config. Requesting path to propagation node to get target propagation cost... " , RNS . LOG_DEBUG )
RNS . Transport . request_path ( pn_destination_hash )
timeout = time . time ( ) + LXMRouter . PATH_REQUEST_WAIT
while not RNS . Identity . recall_app_data ( pn_destination_hash ) and time . time ( ) < timeout :
time . sleep ( 0.5 )
pn_app_data = RNS . Identity . recall_app_data ( pn_destination_hash )
if pn_announce_data_is_valid ( pn_app_data ) :
pn_config = msgpack . unpackb ( pn_app_data )
target_propagation_cost = pn_config [ 5 ] [ 0 ]
if not target_propagation_cost : RNS . log ( " Propagation node stamp cost still unavailable after path request " , RNS . LOG_ERROR )
return target_propagation_cost
2024-09-17 13:18:16 +02:00
def set_inbound_propagation_node ( self , destination_hash ) :
# TODO: Implement
raise NotImplementedError ( " Inbound/outbound propagation node differentiation is currently not implemented " )
def get_inbound_propagation_node ( self ) :
return self . get_outbound_propagation_node ( )
2024-03-02 09:09:51 +01:00
def set_retain_node_lxms ( self , retain ) :
if retain == True :
self . retain_synced_on_node = True
else :
self . retain_synced_on_node = False
2022-06-17 13:42:44 +02:00
def set_authentication ( self , required = None ) :
if required != None :
self . auth_required = required
def requires_authentication ( self ) :
return self . auth_required
def allow ( self , identity_hash = None ) :
if isinstance ( identity_hash , bytes ) and len ( identity_hash ) == RNS . Identity . TRUNCATED_HASHLENGTH / / 8 :
2022-06-17 14:07:40 +02:00
if not identity_hash in self . allowed_list :
self . allowed_list . append ( identity_hash )
2022-06-17 13:42:44 +02:00
else :
raise ValueError ( " Allowed identity hash must be " + str ( RNS . Identity . TRUNCATED_HASHLENGTH / / 8 ) + " bytes " )
def disallow ( self , identity_hash = None ) :
if isinstance ( identity_hash , bytes ) and len ( identity_hash ) == RNS . Identity . TRUNCATED_HASHLENGTH / / 8 :
if identity_hash in self . allowed_list :
self . allowed_list . pop ( identity_hash )
else :
raise ValueError ( " Disallowed identity hash must be " + str ( RNS . Identity . TRUNCATED_HASHLENGTH / / 8 ) + " bytes " )
2025-11-01 13:10:28 +01:00
def allow_control ( self , identity_hash = None ) :
if isinstance ( identity_hash , bytes ) and len ( identity_hash ) == RNS . Identity . TRUNCATED_HASHLENGTH / / 8 :
if not identity_hash in self . control_allowed_list : self . control_allowed_list . append ( identity_hash )
else : raise ValueError ( " Allowed identity hash must be " + str ( RNS . Identity . TRUNCATED_HASHLENGTH / / 8 ) + " bytes " )
def disallow_control ( self , identity_hash = None ) :
if isinstance ( identity_hash , bytes ) and len ( identity_hash ) == RNS . Identity . TRUNCATED_HASHLENGTH / / 8 :
if identity_hash in self . control_allowed_list : self . control_allowed_list . pop ( identity_hash )
else : raise ValueError ( " Disallowed identity hash must be " + str ( RNS . Identity . TRUNCATED_HASHLENGTH / / 8 ) + " bytes " )
2022-06-17 14:07:40 +02:00
def prioritise ( self , destination_hash = None ) :
if isinstance ( destination_hash , bytes ) and len ( destination_hash ) == RNS . Reticulum . TRUNCATED_HASHLENGTH / / 8 :
if not destination_hash in self . prioritised_list :
self . prioritised_list . append ( destination_hash )
else :
raise ValueError ( " Prioritised destination hash must be " + str ( RNS . Reticulum . TRUNCATED_HASHLENGTH / / 8 ) + " bytes " )
def unprioritise ( self , identity_hash = None ) :
if isinstance ( destination_hash , bytes ) and len ( destination_hash ) == RNS . Reticulum . TRUNCATED_HASHLENGTH / / 8 :
if destination_hash in self . prioritised_list :
self . prioritised_list . pop ( destination_hash )
else :
raise ValueError ( " Prioritised destination hash must be " + str ( RNS . Reticulum . TRUNCATED_HASHLENGTH / / 8 ) + " bytes " )
2022-06-17 08:54:04 +02:00
def request_messages_from_propagation_node ( self , identity , max_messages = PR_ALL_MESSAGES ) :
if max_messages == None :
max_messages = LXMRouter . PR_ALL_MESSAGES
2023-10-28 00:06:56 +02:00
self . propagation_transfer_progress = 0.0
2022-06-17 08:54:04 +02:00
self . propagation_transfer_max_messages = max_messages
if self . outbound_propagation_node != None :
if self . outbound_propagation_link != None and self . outbound_propagation_link . status == RNS . Link . ACTIVE :
self . propagation_transfer_state = LXMRouter . PR_LINK_ESTABLISHED
2024-11-23 13:20:24 +01:00
RNS . log ( " Requesting message list from propagation node " , RNS . LOG_DEBUG )
2022-06-17 08:54:04 +02:00
self . outbound_propagation_link . identify ( identity )
self . outbound_propagation_link . request (
LXMPeer . MESSAGE_GET_PATH ,
2024-03-02 09:09:51 +01:00
[ None , None ] , # Set both want and have fields to None to get message list
2022-06-17 08:54:04 +02:00
response_callback = self . message_list_response ,
failed_callback = self . message_get_failed
)
self . propagation_transfer_state = LXMRouter . PR_REQUEST_SENT
else :
if self . outbound_propagation_link == None :
if RNS . Transport . has_path ( self . outbound_propagation_node ) :
self . wants_download_on_path_available_from = None
self . propagation_transfer_state = LXMRouter . PR_LINK_ESTABLISHING
RNS . log ( " Establishing link to " + RNS . prettyhexrep ( self . outbound_propagation_node ) + " for message download " , RNS . LOG_DEBUG )
propagation_node_identity = RNS . Identity . recall ( self . outbound_propagation_node )
propagation_node_destination = RNS . Destination ( propagation_node_identity , RNS . Destination . OUT , RNS . Destination . SINGLE , APP_NAME , " propagation " )
def msg_request_established_callback ( link ) :
self . request_messages_from_propagation_node ( identity , self . propagation_transfer_max_messages )
self . outbound_propagation_link = RNS . Link ( propagation_node_destination , established_callback = msg_request_established_callback )
else :
RNS . log ( " No path known for message download from propagation node " + RNS . prettyhexrep ( self . outbound_propagation_node ) + " . Requesting path... " , RNS . LOG_DEBUG )
RNS . Transport . request_path ( self . outbound_propagation_node )
self . wants_download_on_path_available_from = self . outbound_propagation_node
self . wants_download_on_path_available_to = identity
self . wants_download_on_path_available_timeout = time . time ( ) + LXMRouter . PR_PATH_TIMEOUT
self . propagation_transfer_state = LXMRouter . PR_PATH_REQUESTED
self . request_messages_path_job ( )
else :
RNS . log ( " Waiting for propagation node link to become active " , RNS . LOG_EXTREME )
else :
RNS . log ( " Cannot request LXMF propagation node sync, no default propagation node configured " , RNS . LOG_WARNING )
2022-06-17 09:15:56 +02:00
def cancel_propagation_node_requests ( self ) :
2022-06-17 08:54:04 +02:00
if self . outbound_propagation_link != None :
self . outbound_propagation_link . teardown ( )
2022-06-17 09:15:56 +02:00
self . outbound_propagation_link = None
2022-06-17 08:54:04 +02:00
2022-06-17 13:42:44 +02:00
self . acknowledge_sync_completion ( reset_state = True )
2022-06-17 08:54:04 +02:00
def enable_propagation ( self ) :
try :
self . messagepath = self . storagepath + " /messagestore "
if not os . path . isdir ( self . storagepath ) :
os . makedirs ( self . storagepath )
if not os . path . isdir ( self . messagepath ) :
os . makedirs ( self . messagepath )
self . propagation_entries = { }
2025-01-21 10:46:59 +01:00
st = time . time ( ) ; RNS . log ( " Indexing messagestore... " , RNS . LOG_NOTICE )
2022-06-17 08:54:04 +02:00
for filename in os . listdir ( self . messagepath ) :
components = filename . split ( " _ " )
2025-11-02 12:02:21 +01:00
if len ( components ) > = 3 :
2022-06-17 08:54:04 +02:00
if float ( components [ 1 ] ) > 0 :
if len ( components [ 0 ] ) == RNS . Identity . HASHLENGTH / / 8 * 2 :
2022-06-17 12:37:00 +02:00
try :
2025-10-30 21:19:38 +01:00
transient_id = bytes . fromhex ( components [ 0 ] )
received = float ( components [ 1 ] )
2025-11-02 12:02:21 +01:00
stamp_value = int ( components [ 2 ] )
2025-10-30 21:19:38 +01:00
filepath = self . messagepath + " / " + filename
msg_size = os . path . getsize ( filepath )
file = open ( filepath , " rb " )
destination_hash = file . read ( LXMessage . DESTINATION_LENGTH )
2022-06-17 12:37:00 +02:00
file . close ( )
self . propagation_entries [ transient_id ] = [
2025-01-21 16:33:39 +01:00
destination_hash , # 0: Destination hash
filepath , # 1: Storage location
received , # 2: Receive timestamp
msg_size , # 3: Message size
[ ] , # 4: Handled peers
[ ] , # 5: Unhandled peers
2025-10-30 21:19:38 +01:00
stamp_value , # 6: Stamp value
2022-06-17 12:37:00 +02:00
]
except Exception as e :
RNS . log ( " Could not read LXM from message store. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2022-06-17 08:54:04 +02:00
2025-01-30 00:37:50 +01:00
et = time . time ( ) ; mps = 0 if et - st == 0 else math . floor ( len ( self . propagation_entries ) / ( et - st ) )
RNS . log ( f " Indexed { len ( self . propagation_entries ) } messages in { RNS . prettytime ( et - st ) } , { mps } msgs/s " , RNS . LOG_NOTICE )
RNS . log ( " Rebuilding peer synchronisation states... " , RNS . LOG_NOTICE )
2025-10-30 12:41:18 +01:00
st = time . time ( )
2025-01-21 10:46:59 +01:00
2022-06-17 08:54:04 +02:00
if os . path . isfile ( self . storagepath + " /peers " ) :
peers_file = open ( self . storagepath + " /peers " , " rb " )
peers_data = peers_file . read ( )
2025-01-21 10:46:59 +01:00
peers_file . close ( )
2022-06-17 08:54:04 +02:00
if len ( peers_data ) > 0 :
serialised_peers = msgpack . unpackb ( peers_data )
2025-01-21 16:33:39 +01:00
del peers_data
2022-06-17 08:54:04 +02:00
2025-01-21 16:33:39 +01:00
while len ( serialised_peers ) > 0 :
serialised_peer = serialised_peers . pop ( )
2022-06-17 08:54:04 +02:00
peer = LXMPeer . from_bytes ( serialised_peer , self )
2025-01-21 16:33:39 +01:00
del serialised_peer
2025-01-22 01:37:09 +01:00
if peer . destination_hash in self . static_peers and peer . last_heard == 0 :
RNS . Transport . request_path ( peer . destination_hash )
2022-06-17 08:54:04 +02:00
if peer . identity != None :
self . peers [ peer . destination_hash ] = peer
2024-03-01 22:37:54 +01:00
lim_str = " , no transfer limit "
if peer . propagation_transfer_limit != None :
lim_str = " , " + RNS . prettysize ( peer . propagation_transfer_limit * 1000 ) + " transfer limit "
2025-01-21 16:33:39 +01:00
RNS . log ( " Rebuilt peer " + RNS . prettyhexrep ( peer . destination_hash ) + " with " + str ( peer . unhandled_message_count ) + " unhandled messages " + lim_str , RNS . LOG_DEBUG )
2022-06-17 08:54:04 +02:00
else :
RNS . log ( " Peer " + RNS . prettyhexrep ( peer . destination_hash ) + " could not be loaded, because its identity could not be recalled. Dropping peer. " , RNS . LOG_DEBUG )
2025-01-21 16:33:39 +01:00
del peer
2022-06-17 08:54:04 +02:00
2025-01-21 10:46:59 +01:00
del serialised_peers
2025-01-22 01:37:09 +01:00
if len ( self . static_peers ) > 0 :
for static_peer in self . static_peers :
if not static_peer in self . peers :
RNS . log ( f " Activating static peering with { RNS . prettyhexrep ( static_peer ) } " , RNS . LOG_NOTICE )
2025-10-29 23:24:45 +01:00
self . peers [ static_peer ] = LXMPeer ( self , static_peer , sync_strategy = self . default_sync_strategy )
2025-01-22 01:37:09 +01:00
if self . peers [ static_peer ] . last_heard == 0 :
# TODO: Allow path request responses through announce handler
# momentarily here, so peering config can be updated even if
# the static peer is not available to directly send an announce.
RNS . Transport . request_path ( static_peer )
2025-01-21 16:33:39 +01:00
RNS . log ( f " Rebuilt synchronisation state for { len ( self . peers ) } peers in { RNS . prettytime ( time . time ( ) - st ) } " , RNS . LOG_NOTICE )
2022-06-17 08:54:04 +02:00
2025-01-23 14:13:08 +01:00
try :
if os . path . isfile ( self . storagepath + " /node_stats " ) :
node_stats_file = open ( self . storagepath + " /node_stats " , " rb " )
data = node_stats_file . read ( )
node_stats_file . close ( )
node_stats = msgpack . unpackb ( data )
if not type ( node_stats ) == dict :
RNS . log ( " Invalid data format for loaded local node stats, node stats will be reset " , RNS . LOG_ERROR )
else :
self . client_propagation_messages_received = node_stats [ " client_propagation_messages_received " ]
self . client_propagation_messages_served = node_stats [ " client_propagation_messages_served " ]
self . unpeered_propagation_incoming = node_stats [ " unpeered_propagation_incoming " ]
self . unpeered_propagation_rx_bytes = node_stats [ " unpeered_propagation_rx_bytes " ]
except Exception as e :
RNS . log ( " Could not load local node stats. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2022-06-17 08:54:04 +02:00
self . propagation_node = True
2025-01-23 14:13:08 +01:00
self . propagation_node_start_time = time . time ( )
2022-06-17 08:54:04 +02:00
self . propagation_destination . set_link_established_callback ( self . propagation_link_established )
self . propagation_destination . set_packet_callback ( self . propagation_packet )
self . propagation_destination . register_request_handler ( LXMPeer . OFFER_REQUEST_PATH , self . offer_request , allow = RNS . Destination . ALLOW_ALL )
self . propagation_destination . register_request_handler ( LXMPeer . MESSAGE_GET_PATH , self . message_get_request , allow = RNS . Destination . ALLOW_ALL )
2025-11-01 13:10:28 +01:00
self . control_allowed_list = [ self . identity . hash ]
self . control_destination = RNS . Destination ( self . identity , RNS . Destination . IN , RNS . Destination . SINGLE , APP_NAME , " propagation " , " control " )
self . control_destination . register_request_handler ( LXMRouter . STATS_GET_PATH , self . stats_get_request , allow = RNS . Destination . ALLOW_LIST , allowed_list = self . control_allowed_list )
self . control_destination . register_request_handler ( LXMRouter . SYNC_REQUEST_PATH , self . peer_sync_request , allow = RNS . Destination . ALLOW_LIST , allowed_list = self . control_allowed_list )
2025-11-01 17:32:45 +01:00
self . control_destination . register_request_handler ( LXMRouter . UNPEER_REQUEST_PATH , self . peer_unpeer_request , allow = RNS . Destination . ALLOW_LIST , allowed_list = self . control_allowed_list )
2025-01-23 14:13:08 +01:00
2022-06-17 15:17:27 +02:00
if self . message_storage_limit != None :
limit_str = " , limit is " + RNS . prettysize ( self . message_storage_limit )
else :
limit_str = " "
RNS . log ( " LXMF Propagation Node message store size is " + RNS . prettysize ( self . message_storage_size ( ) ) + limit_str , RNS . LOG_DEBUG )
2022-06-17 12:37:00 +02:00
2022-06-17 08:54:04 +02:00
self . announce_propagation_node ( )
except Exception as e :
RNS . log ( " Could not enable propagation node. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
raise e
RNS . panic ( )
def disable_propagation ( self ) :
self . propagation_node = False
self . announce_propagation_node ( )
2024-09-06 20:34:09 +02:00
def enforce_stamps ( self ) :
self . _enforce_stamps = True
def ignore_stamps ( self ) :
self . _enforce_stamps = False
2022-06-17 09:15:56 +02:00
def ignore_destination ( self , destination_hash ) :
if not destination_hash in self . ignored_list :
self . ignored_list . append ( destination_hash )
2022-06-17 08:54:04 +02:00
2022-06-17 09:15:56 +02:00
def unignore_destination ( self , destination_hash ) :
if destination_hash in self . ignored_list :
self . ignored_list . remove ( destination_hash )
2022-06-17 08:54:04 +02:00
2022-06-17 12:37:00 +02:00
def set_message_storage_limit ( self , kilobytes = None , megabytes = None , gigabytes = None ) :
limit_bytes = 0
if kilobytes != None :
2022-06-17 14:07:40 +02:00
limit_bytes + = kilobytes * 1000
2022-06-17 12:37:00 +02:00
if megabytes != None :
2022-06-17 14:07:40 +02:00
limit_bytes + = megabytes * 1000 * 1000
2022-06-17 12:37:00 +02:00
if gigabytes != None :
limit_bytes + = gigabytes * 1000 * 1000 * 1000
if limit_bytes == 0 :
limit_bytes = None
try :
if limit_bytes == None or int ( limit_bytes ) > 0 :
self . message_storage_limit = int ( limit_bytes )
else :
raise ValueError ( " Cannot set LXMF information storage limit to " + str ( limit_bytes ) )
except Exception as e :
raise ValueError ( " Cannot set LXMF information storage limit to " + str ( limit_bytes ) )
def message_storage_limit ( self ) :
return self . message_storage_limit
def message_storage_size ( self ) :
if self . propagation_node :
return sum ( self . propagation_entries [ f ] [ 3 ] for f in self . propagation_entries )
else :
return None
def set_information_storage_limit ( self , kilobytes = None , megabytes = None , gigabytes = None ) :
limit_bytes = 0
2025-11-01 17:35:36 +01:00
if kilobytes != None : limit_bytes + = kilobytes * 1000
if megabytes != None : limit_bytes + = megabytes * 1000 * 1000
if gigabytes != None : limit_bytes + = gigabytes * 1000 * 1000 * 1000
if limit_bytes == 0 : limit_bytes = None
2022-06-17 12:37:00 +02:00
try :
2025-11-01 17:35:36 +01:00
if limit_bytes == None or int ( limit_bytes ) > 0 : self . information_storage_limit = int ( limit_bytes )
else : raise ValueError ( " Cannot set LXMF information storage limit to " + str ( limit_bytes ) )
except Exception as e : raise ValueError ( " Cannot set LXMF information storage limit to " + str ( limit_bytes ) )
2022-06-17 12:37:00 +02:00
def information_storage_limit ( self ) :
return self . information_storage_limit
def information_storage_size ( self ) :
pass
2024-09-16 20:08:26 +02:00
def delivery_link_available ( self , destination_hash ) :
2025-11-01 17:35:36 +01:00
if destination_hash in self . direct_links or destination_hash in self . backchannel_links : return True
else : return False
2024-09-16 20:08:26 +02:00
2022-06-17 08:54:04 +02:00
2025-01-23 14:13:08 +01:00
### Propagation Node Control ##########################
#######################################################
def compile_stats ( self ) :
2025-11-01 17:35:36 +01:00
if not self . propagation_node : return None
2025-01-23 14:13:08 +01:00
else :
peer_stats = { }
for peer_id in self . peers . copy ( ) :
peer = self . peers [ peer_id ]
peer_stats [ peer_id ] = {
" type " : " static " if peer_id in self . static_peers else " discovered " ,
" state " : peer . state ,
" alive " : peer . alive ,
2025-10-31 22:24:55 +01:00
" name " : peer . name ,
2025-01-23 14:13:08 +01:00
" last_heard " : int ( peer . last_heard ) ,
" next_sync_attempt " : peer . next_sync_attempt ,
" last_sync_attempt " : peer . last_sync_attempt ,
" sync_backoff " : peer . sync_backoff ,
" peering_timebase " : peer . peering_timebase ,
" ler " : int ( peer . link_establishment_rate ) ,
" str " : int ( peer . sync_transfer_rate ) ,
" transfer_limit " : peer . propagation_transfer_limit ,
2025-10-29 23:24:45 +01:00
" sync_limit " : peer . propagation_sync_limit ,
2025-10-30 14:08:39 +01:00
" target_stamp_cost " : peer . propagation_stamp_cost ,
" stamp_cost_flexibility " : peer . propagation_stamp_cost_flexibility ,
2025-10-31 13:53:59 +01:00
" peering_cost " : peer . peering_cost ,
" peering_key " : peer . peering_key_value ( ) ,
2025-01-23 14:13:08 +01:00
" network_distance " : RNS . Transport . hops_to ( peer_id ) ,
" rx_bytes " : peer . rx_bytes ,
" tx_bytes " : peer . tx_bytes ,
2025-11-01 18:48:31 +01:00
" acceptance_rate " : peer . acceptance_rate ,
2025-01-23 14:13:08 +01:00
" messages " : {
" offered " : peer . offered ,
" outgoing " : peer . outgoing ,
" incoming " : peer . incoming ,
2025-01-23 16:27:23 +01:00
" unhandled " : peer . unhandled_message_count
2025-01-23 14:13:08 +01:00
} ,
}
node_stats = {
" identity_hash " : self . identity . hash ,
" destination_hash " : self . propagation_destination . hash ,
" uptime " : time . time ( ) - self . propagation_node_start_time ,
" delivery_limit " : self . delivery_per_transfer_limit ,
" propagation_limit " : self . propagation_per_transfer_limit ,
2025-10-29 23:24:45 +01:00
" sync_limit " : self . propagation_per_sync_limit ,
2025-10-30 14:08:39 +01:00
" target_stamp_cost " : self . propagation_stamp_cost ,
" stamp_cost_flexibility " : self . propagation_stamp_cost_flexibility ,
2025-10-31 13:53:59 +01:00
" peering_cost " : self . peering_cost ,
" max_peering_cost " : self . max_peering_cost ,
2025-01-23 14:13:08 +01:00
" autopeer_maxdepth " : self . autopeer_maxdepth ,
" from_static_only " : self . from_static_only ,
" messagestore " : {
" count " : len ( self . propagation_entries ) ,
" bytes " : self . message_storage_size ( ) ,
" limit " : self . message_storage_limit ,
} ,
" clients " : {
" client_propagation_messages_received " : self . client_propagation_messages_received ,
" client_propagation_messages_served " : self . client_propagation_messages_served ,
} ,
" unpeered_propagation_incoming " : self . unpeered_propagation_incoming ,
" unpeered_propagation_rx_bytes " : self . unpeered_propagation_rx_bytes ,
" static_peers " : len ( self . static_peers ) ,
" discovered_peers " : len ( self . peers ) - len ( self . static_peers ) ,
" total_peers " : len ( self . peers ) ,
" max_peers " : self . max_peers ,
" peers " : peer_stats ,
}
return node_stats
def stats_get_request ( self , path , data , request_id , remote_identity , requested_at ) :
2025-11-01 13:10:28 +01:00
if remote_identity == None : return LXMPeer . ERROR_NO_IDENTITY
elif remote_identity . hash not in self . control_allowed_list : return LXMPeer . ERROR_NO_ACCESS
else : return self . compile_stats ( )
2025-01-23 14:13:08 +01:00
2025-11-01 00:10:30 +01:00
def peer_sync_request ( self , path , data , request_id , remote_identity , requested_at ) :
2025-11-01 13:10:28 +01:00
if remote_identity == None : return LXMPeer . ERROR_NO_IDENTITY
elif remote_identity . hash not in self . control_allowed_list : return LXMPeer . ERROR_NO_ACCESS
2025-11-01 00:10:30 +01:00
else :
if type ( data ) != bytes : return LXMPeer . ERROR_INVALID_DATA
elif len ( data ) != RNS . Identity . TRUNCATED_HASHLENGTH / / 8 : return LXMPeer . ERROR_INVALID_DATA
else :
if not data in self . peers : return LXMPeer . ERROR_NOT_FOUND
else :
self . peers [ data ] . sync ( )
return True
2025-11-01 17:32:45 +01:00
def peer_unpeer_request ( self , path , data , request_id , remote_identity , requested_at ) :
if remote_identity == None : return LXMPeer . ERROR_NO_IDENTITY
elif remote_identity . hash not in self . control_allowed_list : return LXMPeer . ERROR_NO_ACCESS
else :
if type ( data ) != bytes : return LXMPeer . ERROR_INVALID_DATA
elif len ( data ) != RNS . Identity . TRUNCATED_HASHLENGTH / / 8 : return LXMPeer . ERROR_INVALID_DATA
else :
if not data in self . peers : return LXMPeer . ERROR_NOT_FOUND
else :
self . unpeer ( data )
return True
2025-01-23 14:13:08 +01:00
2022-06-17 09:15:56 +02:00
### Utility & Maintenance #############################
#######################################################
2022-06-17 08:54:04 +02:00
2022-06-17 09:15:56 +02:00
JOB_OUTBOUND_INTERVAL = 1
2024-09-07 22:40:13 +02:00
JOB_STAMPS_INTERVAL = 1
2022-06-17 09:15:56 +02:00
JOB_LINKS_INTERVAL = 1
JOB_TRANSIENT_INTERVAL = 60
JOB_STORE_INTERVAL = 120
2025-10-29 23:25:15 +01:00
JOB_PEERSYNC_INTERVAL = 6
2025-01-21 20:20:39 +01:00
JOB_PEERINGEST_INTERVAL = JOB_PEERSYNC_INTERVAL
2025-01-30 10:46:31 +01:00
JOB_ROTATE_INTERVAL = 56 * JOB_PEERINGEST_INTERVAL
2022-06-17 09:15:56 +02:00
def jobs ( self ) :
2025-01-21 16:33:39 +01:00
if not self . exit_handler_running :
self . processing_count + = 1
2022-06-17 09:15:56 +02:00
2025-01-21 16:33:39 +01:00
if self . processing_count % LXMRouter . JOB_OUTBOUND_INTERVAL == 0 :
self . process_outbound ( )
2022-06-17 09:15:56 +02:00
2025-01-21 16:33:39 +01:00
if self . processing_count % LXMRouter . JOB_STAMPS_INTERVAL == 0 :
threading . Thread ( target = self . process_deferred_stamps , daemon = True ) . start ( )
2024-09-07 22:40:13 +02:00
2025-01-21 16:33:39 +01:00
if self . processing_count % LXMRouter . JOB_LINKS_INTERVAL == 0 :
self . clean_links ( )
2022-06-17 09:15:56 +02:00
2025-01-21 16:33:39 +01:00
if self . processing_count % LXMRouter . JOB_TRANSIENT_INTERVAL == 0 :
self . clean_transient_id_caches ( )
2022-06-17 09:15:56 +02:00
2025-01-21 16:33:39 +01:00
if self . processing_count % LXMRouter . JOB_STORE_INTERVAL == 0 :
2025-11-01 01:35:15 +01:00
if self . propagation_node == True : self . clean_message_store ( )
2022-06-17 09:15:56 +02:00
2025-01-21 20:20:39 +01:00
if self . processing_count % LXMRouter . JOB_PEERINGEST_INTERVAL == 0 :
2025-11-01 01:35:15 +01:00
if self . propagation_node == True : self . flush_queues ( )
2025-01-21 20:20:39 +01:00
2025-01-29 14:24:09 +01:00
if self . processing_count % LXMRouter . JOB_ROTATE_INTERVAL == 0 :
2025-11-01 01:35:15 +01:00
if self . propagation_node == True : self . rotate_peers ( )
2025-01-29 14:24:09 +01:00
2025-01-21 16:33:39 +01:00
if self . processing_count % LXMRouter . JOB_PEERSYNC_INTERVAL == 0 :
2025-11-01 01:35:15 +01:00
if self . propagation_node == True : self . sync_peers ( )
self . clean_throttled_peers ( )
2022-06-17 09:15:56 +02:00
2022-06-17 12:37:00 +02:00
def jobloop ( self ) :
while ( True ) :
# TODO: Improve this to scheduling, so manual
# triggers can delay next run
2023-02-17 19:36:20 +01:00
try :
self . jobs ( )
except Exception as e :
RNS . log ( " An error ocurred while running LXMF Router jobs. " , RNS . LOG_ERROR )
RNS . log ( " The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2025-01-21 16:33:39 +01:00
RNS . trace_exception ( e )
2022-06-17 12:37:00 +02:00
time . sleep ( LXMRouter . PROCESSING_INTERVAL )
2025-01-21 20:20:39 +01:00
def flush_queues ( self ) :
2025-01-21 20:44:11 +01:00
if len ( self . peers ) > 0 :
self . flush_peer_distribution_queue ( )
RNS . log ( " Calculating peer distribution queue mappings... " , RNS . LOG_DEBUG ) ; st = time . time ( )
for peer_id in self . peers . copy ( ) :
if peer_id in self . peers :
peer = self . peers [ peer_id ]
if peer . queued_items ( ) :
peer . process_queues ( )
RNS . log ( f " Distribution queue mapping completed in { RNS . prettytime ( time . time ( ) - st ) } " , RNS . LOG_DEBUG )
2025-01-21 20:20:39 +01:00
2022-06-17 09:15:56 +02:00
def clean_links ( self ) :
closed_links = [ ]
for link_hash in self . direct_links :
link = self . direct_links [ link_hash ]
2025-05-15 20:27:19 +02:00
inactive_time = link . no_data_for ( )
2022-06-17 09:15:56 +02:00
if inactive_time > LXMRouter . LINK_MAX_INACTIVITY :
link . teardown ( )
closed_links . append ( link_hash )
2025-11-02 18:35:15 +01:00
if link . link_id in self . validated_peer_links :
self . validated_peer_links . pop ( link . link_id )
2022-06-17 09:15:56 +02:00
for link_hash in closed_links :
cleaned_link = self . direct_links . pop ( link_hash )
RNS . log ( " Cleaned link " + str ( cleaned_link ) , RNS . LOG_DEBUG )
2024-02-29 23:02:16 +01:00
try :
inactive_links = [ ]
for link in self . active_propagation_links :
if link . no_data_for ( ) > LXMRouter . P_LINK_MAX_INACTIVITY :
inactive_links . append ( link )
for link in inactive_links :
self . active_propagation_links . remove ( link )
link . teardown ( )
except Exception as e :
RNS . log ( " An error occurred while cleaning inbound propagation links. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2022-06-17 09:15:56 +02:00
if self . outbound_propagation_link != None and self . outbound_propagation_link . status == RNS . Link . CLOSED :
self . outbound_propagation_link = None
2023-10-28 00:06:56 +02:00
if self . propagation_transfer_state == LXMRouter . PR_COMPLETE :
self . acknowledge_sync_completion ( )
elif self . propagation_transfer_state < LXMRouter . PR_LINK_ESTABLISHED :
self . acknowledge_sync_completion ( failure_state = LXMRouter . PR_LINK_FAILED )
elif self . propagation_transfer_state > = LXMRouter . PR_LINK_ESTABLISHED and self . propagation_transfer_state < LXMRouter . PR_COMPLETE :
self . acknowledge_sync_completion ( failure_state = LXMRouter . PR_TRANSFER_FAILED )
else :
RNS . log ( f " Unknown propagation transfer state on link cleaning: { self . propagation_transfer_state } " , RNS . LOG_DEBUG )
self . acknowledge_sync_completion ( )
2022-06-17 09:15:56 +02:00
RNS . log ( " Cleaned outbound propagation link " , RNS . LOG_DEBUG )
2022-11-19 20:07:00 +01:00
def clean_transient_id_caches ( self ) :
2022-06-17 09:15:56 +02:00
now = time . time ( )
removed_entries = [ ]
for transient_id in self . locally_delivered_transient_ids :
timestamp = self . locally_delivered_transient_ids [ transient_id ]
2024-11-23 13:20:24 +01:00
if now > timestamp + LXMRouter . MESSAGE_EXPIRY * 6.0 :
2022-06-17 09:15:56 +02:00
removed_entries . append ( transient_id )
for transient_id in removed_entries :
self . locally_delivered_transient_ids . pop ( transient_id )
RNS . log ( " Cleaned " + RNS . prettyhexrep ( transient_id ) + " from local delivery cache " , RNS . LOG_DEBUG )
2022-11-19 20:07:00 +01:00
removed_entries = [ ]
for transient_id in self . locally_processed_transient_ids :
timestamp = self . locally_processed_transient_ids [ transient_id ]
2024-11-23 13:20:24 +01:00
if now > timestamp + LXMRouter . MESSAGE_EXPIRY * 6.0 :
2022-11-19 20:07:00 +01:00
removed_entries . append ( transient_id )
for transient_id in removed_entries :
self . locally_processed_transient_ids . pop ( transient_id )
RNS . log ( " Cleaned " + RNS . prettyhexrep ( transient_id ) + " from locally processed cache " , RNS . LOG_DEBUG )
2024-09-06 20:34:09 +02:00
def update_stamp_cost ( self , destination_hash , stamp_cost ) :
RNS . log ( f " Updating outbound stamp cost for { RNS . prettyhexrep ( destination_hash ) } to { stamp_cost } " , RNS . LOG_DEBUG )
self . outbound_stamp_costs [ destination_hash ] = [ time . time ( ) , stamp_cost ]
2025-10-31 00:28:06 +01:00
def job ( ) : self . save_outbound_stamp_costs ( )
2024-09-06 20:34:09 +02:00
threading . Thread ( target = self . save_outbound_stamp_costs , daemon = True ) . start ( )
def get_announce_app_data ( self , destination_hash ) :
if destination_hash in self . delivery_destinations :
delivery_destination = self . delivery_destinations [ destination_hash ]
display_name = None
if delivery_destination . display_name != None :
display_name = delivery_destination . display_name . encode ( " utf-8 " )
stamp_cost = None
if delivery_destination . stamp_cost != None and type ( delivery_destination . stamp_cost ) == int :
if delivery_destination . stamp_cost > 0 and delivery_destination . stamp_cost < 255 :
stamp_cost = delivery_destination . stamp_cost
peer_data = [ display_name , stamp_cost ]
return msgpack . packb ( peer_data )
2025-10-30 21:19:38 +01:00
def get_size ( self , transient_id ) :
2024-03-01 22:37:54 +01:00
lxm_size = self . propagation_entries [ transient_id ] [ 3 ]
2025-10-30 21:19:38 +01:00
return lxm_size
2024-03-01 22:37:54 +01:00
2025-10-30 21:19:38 +01:00
def get_weight ( self , transient_id ) :
dst_hash = self . propagation_entries [ transient_id ] [ 0 ]
lxm_rcvd = self . propagation_entries [ transient_id ] [ 2 ]
lxm_size = self . propagation_entries [ transient_id ] [ 3 ]
now = time . time ( )
2024-03-01 22:37:54 +01:00
age_weight = max ( 1 , ( now - lxm_rcvd ) / 60 / 60 / 24 / 4 )
2025-10-30 21:19:38 +01:00
if dst_hash in self . prioritised_list : priority_weight = 0.1
else : priority_weight = 1.0
2024-03-01 22:37:54 +01:00
2025-10-30 21:19:38 +01:00
return priority_weight * age_weight * lxm_size
2024-03-01 22:37:54 +01:00
2025-10-30 21:19:38 +01:00
def get_stamp_value ( self , transient_id ) :
if not transient_id in self . propagation_entries : return None
else : return self . propagation_entries [ transient_id ] [ 6 ]
2024-03-01 22:37:54 +01:00
2024-09-07 15:17:48 +02:00
def generate_ticket ( self , destination_hash , expiry = LXMessage . TICKET_EXPIRY ) :
now = time . time ( )
ticket = None
if destination_hash in self . available_tickets [ " last_deliveries " ] :
last_delivery = self . available_tickets [ " last_deliveries " ] [ destination_hash ]
elapsed = now - last_delivery
if elapsed < LXMessage . TICKET_INTERVAL :
RNS . log ( f " A ticket for { RNS . prettyhexrep ( destination_hash ) } was already delivered { RNS . prettytime ( elapsed ) } ago, not including another ticket yet " , RNS . LOG_DEBUG )
return None
if destination_hash in self . available_tickets [ " inbound " ] :
for ticket in self . available_tickets [ " inbound " ] [ destination_hash ] :
ticket_entry = self . available_tickets [ " inbound " ] [ destination_hash ] [ ticket ]
expires = ticket_entry [ 0 ] ; validity_left = expires - now
if validity_left > LXMessage . TICKET_RENEW :
RNS . log ( f " Found generated ticket for { RNS . prettyhexrep ( destination_hash ) } with { RNS . prettytime ( validity_left ) } of validity left, re-using this one " , RNS . LOG_DEBUG )
return [ expires , ticket ]
else :
self . available_tickets [ " inbound " ] [ destination_hash ] = { }
RNS . log ( f " No generated tickets for { RNS . prettyhexrep ( destination_hash ) } with enough validity found, generating a new one " , RNS . LOG_DEBUG )
expires = now + expiry
ticket = os . urandom ( LXMessage . TICKET_LENGTH )
self . available_tickets [ " inbound " ] [ destination_hash ] [ ticket ] = [ expires ]
self . save_available_tickets ( )
return [ expires , ticket ]
def remember_ticket ( self , destination_hash , ticket_entry ) :
expires = ticket_entry [ 0 ] - time . time ( )
RNS . log ( f " Remembering ticket for { RNS . prettyhexrep ( destination_hash ) } , expires in { RNS . prettytime ( expires ) } " , RNS . LOG_DEBUG )
self . available_tickets [ " outbound " ] [ destination_hash ] = [ ticket_entry [ 0 ] , ticket_entry [ 1 ] ]
def get_outbound_ticket ( self , destination_hash ) :
if destination_hash in self . available_tickets [ " outbound " ] :
entry = self . available_tickets [ " outbound " ] [ destination_hash ]
if entry [ 0 ] > time . time ( ) :
return entry [ 1 ]
return None
2024-09-07 22:40:13 +02:00
def get_outbound_ticket_expiry ( self , destination_hash ) :
if destination_hash in self . available_tickets [ " outbound " ] :
entry = self . available_tickets [ " outbound " ] [ destination_hash ]
if entry [ 0 ] > time . time ( ) :
return entry [ 0 ]
return None
2024-09-07 15:17:48 +02:00
def get_inbound_tickets ( self , destination_hash ) :
now = time . time ( )
available_tickets = [ ]
if destination_hash in self . available_tickets [ " inbound " ] :
for inbound_ticket in self . available_tickets [ " inbound " ] [ destination_hash ] :
if now < self . available_tickets [ " inbound " ] [ destination_hash ] [ inbound_ticket ] [ 0 ] :
available_tickets . append ( inbound_ticket )
if len ( available_tickets ) == 0 :
return None
else :
return available_tickets
2025-11-01 01:35:15 +01:00
def clean_throttled_peers ( self ) :
expired_entries = [ ]
now = time . time ( )
for peer_hash in self . throttled_peers :
if now > self . throttled_peers [ peer_hash ] : expired_entries . append ( peer_hash )
for peer_hash in expired_entries : self . throttled_peers . pop ( peer_hash )
2022-06-17 09:15:56 +02:00
def clean_message_store ( self ) :
2025-01-24 00:09:36 +01:00
RNS . log ( " Cleaning message store " , RNS . LOG_VERBOSE )
2022-06-17 12:37:00 +02:00
# Check and remove expired messages
2022-06-17 09:15:56 +02:00
now = time . time ( )
removed_entries = { }
2025-01-24 00:09:36 +01:00
for transient_id in self . propagation_entries . copy ( ) :
2025-11-02 11:38:15 +01:00
entry = self . propagation_entries [ transient_id ]
filepath = entry [ 1 ]
2025-11-02 11:35:12 +01:00
stamp_value = entry [ 6 ]
2025-11-02 11:38:15 +01:00
filename = os . path . split ( filepath ) [ - 1 ]
components = filename . split ( " _ " )
2022-06-17 09:15:56 +02:00
2025-11-02 11:38:15 +01:00
if len ( components ) == 3 and float ( components [ 1 ] ) > 0 and len ( os . path . split ( components [ 0 ] ) [ 1 ] ) == ( RNS . Identity . HASHLENGTH / / 8 ) * 2 and int ( components [ 2 ] ) == stamp_value :
2022-06-17 09:15:56 +02:00
timestamp = float ( components [ 1 ] )
if now > timestamp + LXMRouter . MESSAGE_EXPIRY :
2025-01-24 00:09:36 +01:00
RNS . log ( " Purging message " + RNS . prettyhexrep ( transient_id ) + " due to expiry " , RNS . LOG_EXTREME )
2022-06-17 09:15:56 +02:00
removed_entries [ transient_id ] = filepath
else :
RNS . log ( " Purging message " + RNS . prettyhexrep ( transient_id ) + " due to invalid file path " , RNS . LOG_WARNING )
removed_entries [ transient_id ] = filepath
removed_count = 0
for transient_id in removed_entries :
try :
filepath = removed_entries [ transient_id ]
self . propagation_entries . pop ( transient_id )
if os . path . isfile ( filepath ) :
os . unlink ( filepath )
removed_count + = 1
except Exception as e :
RNS . log ( " Could not remove " + RNS . prettyhexrep ( transient_id ) + " from message store. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
if removed_count > 0 :
2025-01-24 00:09:36 +01:00
RNS . log ( " Cleaned " + str ( removed_count ) + " entries from the message store " , RNS . LOG_VERBOSE )
2022-06-17 09:15:56 +02:00
2022-06-17 12:37:00 +02:00
# Check size of message store and cull if needed
try :
message_storage_size = self . message_storage_size ( )
if message_storage_size != None :
if self . message_storage_limit != None and message_storage_size > self . message_storage_limit :
# Clean the message storage according to priorities
bytes_needed = message_storage_size - self . message_storage_limit
bytes_cleaned = 0
weighted_entries = [ ]
2025-01-24 00:09:36 +01:00
for transient_id in self . propagation_entries . copy ( ) :
2024-03-01 22:37:54 +01:00
weighted_entries . append ( [
self . propagation_entries [ transient_id ] ,
self . get_weight ( transient_id ) ,
transient_id
] )
2022-06-17 12:37:00 +02:00
weighted_entries . sort ( key = lambda we : we [ 1 ] , reverse = True )
i = 0
while i < len ( weighted_entries ) and bytes_cleaned < bytes_needed :
try :
w = weighted_entries [ i ]
entry = w [ 0 ]
transient_id = w [ 2 ]
filepath = entry [ 1 ]
if os . path . isfile ( filepath ) :
os . unlink ( filepath )
self . propagation_entries . pop ( transient_id )
bytes_cleaned + = entry [ 3 ]
RNS . log ( " Removed " + RNS . prettyhexrep ( transient_id ) + " with weight " + str ( w [ 1 ] ) + " to clear up " + RNS . prettysize ( entry [ 3 ] ) + " , now cleaned " + RNS . prettysize ( bytes_cleaned ) + " out of " + RNS . prettysize ( bytes_needed ) + " needed " , RNS . LOG_EXTREME )
except Exception as e :
RNS . log ( " Error while cleaning LXMF message from message store. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
finally :
i + = 1
2024-03-02 09:09:51 +01:00
RNS . log ( " LXMF message store size is now " + RNS . prettysize ( self . message_storage_size ( ) ) + " for " + str ( len ( self . propagation_entries ) ) + " items " , RNS . LOG_EXTREME )
2022-06-17 12:37:00 +02:00
except Exception as e :
RNS . log ( " Could not clean the LXMF message store. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2022-10-22 22:35:07 +02:00
def save_locally_delivered_transient_ids ( self ) :
try :
2025-01-21 16:33:39 +01:00
if len ( self . locally_delivered_transient_ids ) > 0 :
if not os . path . isdir ( self . storagepath ) :
2025-01-23 14:13:08 +01:00
os . makedirs ( self . storagepath )
2022-10-22 22:35:07 +02:00
2025-01-21 16:33:39 +01:00
with open ( self . storagepath + " /local_deliveries " , " wb " ) as locally_delivered_file :
locally_delivered_file . write ( msgpack . packb ( self . locally_delivered_transient_ids ) )
2022-10-22 22:35:07 +02:00
except Exception as e :
RNS . log ( " Could not save locally delivered message ID cache to storage. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2022-11-19 20:07:00 +01:00
def save_locally_processed_transient_ids ( self ) :
try :
2025-01-21 16:33:39 +01:00
if len ( self . locally_processed_transient_ids ) > 0 :
if not os . path . isdir ( self . storagepath ) :
2025-01-23 14:13:08 +01:00
os . makedirs ( self . storagepath )
2022-11-19 20:07:00 +01:00
2025-01-21 16:33:39 +01:00
with open ( self . storagepath + " /locally_processed " , " wb " ) as locally_processed_file :
locally_processed_file . write ( msgpack . packb ( self . locally_processed_transient_ids ) )
2022-11-19 20:07:00 +01:00
except Exception as e :
2024-09-08 17:54:11 +02:00
RNS . log ( " Could not save locally processed transient ID cache to storage. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2022-11-19 20:07:00 +01:00
2025-01-23 14:13:08 +01:00
def save_node_stats ( self ) :
try :
if not os . path . isdir ( self . storagepath ) :
os . makedirs ( self . storagepath )
with open ( self . storagepath + " /node_stats " , " wb " ) as stats_file :
node_stats = {
" client_propagation_messages_received " : self . client_propagation_messages_received ,
" client_propagation_messages_served " : self . client_propagation_messages_served ,
" unpeered_propagation_incoming " : self . unpeered_propagation_incoming ,
" unpeered_propagation_rx_bytes " : self . unpeered_propagation_rx_bytes ,
}
stats_file . write ( msgpack . packb ( node_stats ) )
except Exception as e :
RNS . log ( " Could not save local node stats to storage. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2024-09-06 20:34:09 +02:00
def clean_outbound_stamp_costs ( self ) :
try :
expired = [ ]
for destination_hash in self . outbound_stamp_costs :
entry = self . outbound_stamp_costs [ destination_hash ]
if time . time ( ) > entry [ 0 ] + LXMRouter . STAMP_COST_EXPIRY :
expired . append ( destination_hash )
for destination_hash in expired :
self . outbound_stamp_costs . pop ( destination_hash )
except Exception as e :
RNS . log ( f " Error while cleaning outbound stamp costs. The contained exception was: { e } " , RNS . LOG_ERROR )
RNS . trace_exception ( e )
def save_outbound_stamp_costs ( self ) :
with self . cost_file_lock :
try :
if not os . path . isdir ( self . storagepath ) :
os . makedirs ( self . storagepath )
2024-09-07 15:17:48 +02:00
outbound_stamp_costs_file = open ( self . storagepath + " /outbound_stamp_costs " , " wb " )
outbound_stamp_costs_file . write ( msgpack . packb ( self . outbound_stamp_costs ) )
outbound_stamp_costs_file . close ( )
2024-09-06 20:34:09 +02:00
except Exception as e :
2024-09-08 17:54:11 +02:00
RNS . log ( " Could not save outbound stamp costs to storage. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2024-09-06 20:34:09 +02:00
2024-09-07 15:17:48 +02:00
def clean_available_tickets ( self ) :
try :
# Clean outbound tickets
expired_outbound = [ ]
for destination_hash in self . available_tickets [ " outbound " ] :
entry = self . available_tickets [ " outbound " ] [ destination_hash ]
if time . time ( ) > entry [ 0 ] :
expired_outbound . append ( destination_hash )
for destination_hash in expired_outbound :
self . available_tickets [ " outbound " ] . pop ( destination_hash )
# Clean inbound tickets
for destination_hash in self . available_tickets [ " inbound " ] :
expired_inbound = [ ]
for inbound_ticket in self . available_tickets [ " inbound " ] [ destination_hash ] :
entry = self . available_tickets [ " inbound " ] [ destination_hash ] [ inbound_ticket ]
ticket_expiry = entry [ 0 ]
if time . time ( ) > ticket_expiry + LXMessage . TICKET_GRACE :
expired_inbound . append ( inbound_ticket )
for inbound_ticket in expired_inbound :
2024-10-05 15:26:18 +02:00
self . available_tickets [ " inbound " ] [ destination_hash ] . pop ( inbound_ticket )
2024-09-07 15:17:48 +02:00
except Exception as e :
2024-09-08 17:54:11 +02:00
RNS . log ( f " Error while cleaning available tickets. The contained exception was: { e } " , RNS . LOG_ERROR )
2024-09-07 15:17:48 +02:00
RNS . trace_exception ( e )
def save_available_tickets ( self ) :
with self . ticket_file_lock :
try :
if not os . path . isdir ( self . storagepath ) :
os . makedirs ( self . storagepath )
available_tickets_file = open ( self . storagepath + " /available_tickets " , " wb " )
available_tickets_file . write ( msgpack . packb ( self . available_tickets ) )
available_tickets_file . close ( )
except Exception as e :
RNS . log ( " Could not save available tickets to storage. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2024-09-07 22:40:13 +02:00
def reload_available_tickets ( self ) :
RNS . log ( " Reloading available tickets from storage " , RNS . LOG_DEBUG )
try :
with self . ticket_file_lock :
with open ( self . storagepath + " /available_tickets " , " rb " ) as available_tickets_file :
data = available_tickets_file . read ( )
self . available_tickets = msgpack . unpackb ( data )
if not type ( self . available_tickets ) == dict :
RNS . log ( " Invalid data format for loaded available tickets, recreating... " , RNS . LOG_ERROR )
self . available_tickets = { " outbound " : { } , " inbound " : { } , " last_deliveries " : { } }
if not " outbound " in self . available_tickets :
RNS . log ( " Missing outbound entry in loaded available tickets, recreating... " , RNS . LOG_ERROR )
self . available_tickets [ " outbound " ] = { }
if not " inbound " in self . available_tickets :
RNS . log ( " Missing inbound entry in loaded available tickets, recreating... " , RNS . LOG_ERROR )
self . available_tickets [ " inbound " ] = { }
if not " last_deliveries " in self . available_tickets :
RNS . log ( " Missing local_deliveries entry in loaded available tickets, recreating... " , RNS . LOG_ERROR )
self . available_tickets [ " last_deliveries " ] = { }
except Exception as e :
RNS . log ( f " An error occurred while reloading available tickets from storage: { e } " , RNS . LOG_ERROR )
2022-06-17 09:15:56 +02:00
def exit_handler ( self ) :
2025-01-21 16:33:39 +01:00
if self . exit_handler_running :
return
self . exit_handler_running = True
RNS . log ( " Tearing down delivery destinations... " , RNS . LOG_NOTICE )
for destination_hash in self . delivery_destinations :
delivery_destination = self . delivery_destinations [ destination_hash ]
delivery_destination . set_packet_callback ( None )
delivery_destination . set_link_established_callback ( None )
for link in delivery_destination . links :
try :
if link . status == RNS . Link . ACTIVE :
link . teardown ( )
except Exception as e :
RNS . log ( " Error while tearing down propagation link: {e} " , RNS . LOG_ERROR )
if self . propagation_node :
RNS . log ( " Tearing down propagation node destination... " , RNS . LOG_NOTICE )
self . propagation_destination . set_link_established_callback ( None )
self . propagation_destination . set_packet_callback ( None )
self . propagation_destination . deregister_request_handler ( LXMPeer . OFFER_REQUEST_PATH )
self . propagation_destination . deregister_request_handler ( LXMPeer . MESSAGE_GET_PATH )
2025-01-23 14:13:08 +01:00
self . propagation_destination . deregister_request_handler ( LXMRouter . STATS_GET_PATH )
2025-11-01 17:32:45 +01:00
self . propagation_destination . deregister_request_handler ( LXMRouter . SYNC_REQUEST_PATH )
self . propagation_destination . deregister_request_handler ( LXMRouter . UNPEER_REQUEST_PATH )
2025-01-21 16:33:39 +01:00
for link in self . active_propagation_links :
try :
if link . status == RNS . Link . ACTIVE :
link . teardown ( )
except Exception as e :
RNS . log ( " Error while tearing down propagation link: {e} " , RNS . LOG_ERROR )
RNS . log ( " Persisting LXMF state data to storage... " , RNS . LOG_NOTICE )
2025-01-21 20:20:39 +01:00
self . flush_queues ( )
2022-06-17 09:15:56 +02:00
if self . propagation_node :
try :
2025-01-22 01:37:09 +01:00
st = time . time ( ) ; RNS . log ( f " Saving { len ( self . peers ) } peer synchronisation states to storage... " , RNS . LOG_NOTICE )
2022-06-17 09:15:56 +02:00
serialised_peers = [ ]
2025-01-21 16:33:39 +01:00
peer_dict = self . peers . copy ( )
for peer_id in peer_dict :
2022-06-17 09:15:56 +02:00
peer = self . peers [ peer_id ]
serialised_peers . append ( peer . to_bytes ( ) )
peers_file = open ( self . storagepath + " /peers " , " wb " )
peers_file . write ( msgpack . packb ( serialised_peers ) )
peers_file . close ( )
2025-01-22 01:37:09 +01:00
RNS . log ( f " Saved { len ( serialised_peers ) } peers to storage in { RNS . prettyshorttime ( time . time ( ) - st ) } " , RNS . LOG_NOTICE )
2022-06-17 08:54:04 +02:00
except Exception as e :
2022-06-17 09:15:56 +02:00
RNS . log ( " Could not save propagation node peers to storage. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2022-10-22 22:35:07 +02:00
self . save_locally_delivered_transient_ids ( )
2022-11-19 20:07:00 +01:00
self . save_locally_processed_transient_ids ( )
2025-01-23 14:13:08 +01:00
self . save_node_stats ( )
2022-06-17 09:15:56 +02:00
2025-01-21 16:33:39 +01:00
def sigint_handler ( self , signal , frame ) :
if not self . exit_handler_running :
RNS . log ( " Received SIGINT, shutting down now! " , RNS . LOG_WARNING )
sys . exit ( 0 )
else :
RNS . log ( " Received SIGINT, but exit handler is running, keeping process alive until storage persist is complete " , RNS . LOG_WARNING )
def sigterm_handler ( self , signal , frame ) :
if not self . exit_handler_running :
RNS . log ( " Received SIGTERM, shutting down now! " , RNS . LOG_WARNING )
sys . exit ( 0 )
else :
RNS . log ( " Received SIGTERM, but exit handler is running, keeping process alive until storage persist is complete " , RNS . LOG_WARNING )
2022-06-17 09:15:56 +02:00
def __str__ ( self ) :
return " <LXMRouter " + RNS . hexrep ( self . identity . hash , delimit = False ) + " > "
### Message Download ##################################
#######################################################
def request_messages_path_job ( self ) :
job_thread = threading . Thread ( target = self . __request_messages_path_job )
job_thread . setDaemon ( True )
job_thread . start ( )
def __request_messages_path_job ( self ) :
2024-02-29 20:12:54 +01:00
path_timeout = self . wants_download_on_path_available_timeout
while not RNS . Transport . has_path ( self . wants_download_on_path_available_from ) and time . time ( ) < path_timeout :
2022-06-17 09:15:56 +02:00
time . sleep ( 0.1 )
if RNS . Transport . has_path ( self . wants_download_on_path_available_from ) :
self . request_messages_from_propagation_node ( self . wants_download_on_path_available_to , self . propagation_transfer_max_messages )
else :
RNS . log ( " Propagation node path request timed out " , RNS . LOG_DEBUG )
2023-10-28 00:06:56 +02:00
self . acknowledge_sync_completion ( failure_state = LXMRouter . PR_NO_PATH )
2022-06-17 09:15:56 +02:00
2022-06-17 13:42:44 +02:00
def identity_allowed ( self , identity ) :
if self . auth_required :
if identity . hash in self . allowed_list :
return True
else :
return False
else :
return True
2022-06-17 08:54:04 +02:00
def message_get_request ( self , path , data , request_id , remote_identity , requested_at ) :
2025-11-01 00:10:30 +01:00
if remote_identity == None : return LXMPeer . ERROR_NO_IDENTITY
elif not self . identity_allowed ( remote_identity ) : return LXMPeer . ERROR_NO_ACCESS
2022-06-17 08:54:04 +02:00
else :
try :
remote_destination = RNS . Destination ( remote_identity , RNS . Destination . OUT , RNS . Destination . SINGLE , APP_NAME , " delivery " )
# If both want and have fields are empty, send a list of
# available messages.
if data [ 0 ] == None and data [ 1 ] == None :
available_messages = [ ]
for transient_id in self . propagation_entries :
message_entry = self . propagation_entries [ transient_id ]
if message_entry [ 0 ] == remote_destination . hash :
message_size = os . path . getsize ( message_entry [ 1 ] )
available_entry = [ transient_id , message_size ]
available_messages . append ( available_entry )
available_messages . sort ( key = lambda e : e [ 1 ] , reverse = False )
transient_ids = [ ]
2025-11-01 00:10:30 +01:00
for available_entry in available_messages : transient_ids . append ( available_entry [ 0 ] )
2022-06-17 08:54:04 +02:00
return transient_ids
2022-06-17 09:15:56 +02:00
else :
# Process messages the client already have
if data [ 1 ] != None and len ( data [ 1 ] ) > 0 :
for transient_id in data [ 1 ] :
if transient_id in self . propagation_entries and self . propagation_entries [ transient_id ] [ 0 ] == remote_destination . hash :
try :
filepath = self . propagation_entries [ transient_id ] [ 1 ]
self . propagation_entries . pop ( transient_id )
os . unlink ( filepath )
2025-11-01 18:48:31 +01:00
# TODO: Remove debug
# RNS.log("Client "+RNS.prettyhexrep(remote_destination.hash)+" purged message "+RNS.prettyhexrep(transient_id)+" at "+str(filepath), RNS.LOG_DEBUG)
2022-06-17 09:15:56 +02:00
except Exception as e :
RNS . log ( " Error while processing message purge request from " + RNS . prettyhexrep ( remote_destination . hash ) + " . The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
# Process wanted messages
response_messages = [ ]
if data [ 0 ] != None and len ( data [ 0 ] ) > 0 :
2024-03-02 09:09:51 +01:00
client_transfer_limit = None
if len ( data ) > = 3 :
try :
client_transfer_limit = float ( data [ 2 ] ) * 1000
RNS . log ( " Client indicates transfer limit of " + RNS . prettysize ( client_transfer_limit ) , RNS . LOG_DEBUG )
2025-11-01 00:10:30 +01:00
except : pass
2024-03-02 09:09:51 +01:00
per_message_overhead = 16 # Really only 2 bytes, but set a bit higher for now
cumulative_size = 24 # Initialised to highest reasonable binary structure overhead
2022-06-17 09:15:56 +02:00
for transient_id in data [ 0 ] :
if transient_id in self . propagation_entries and self . propagation_entries [ transient_id ] [ 0 ] == remote_destination . hash :
try :
filepath = self . propagation_entries [ transient_id ] [ 1 ]
RNS . log ( " Client " + RNS . prettyhexrep ( remote_destination . hash ) + " requested message " + RNS . prettyhexrep ( transient_id ) + " at " + str ( filepath ) , RNS . LOG_DEBUG )
message_file = open ( filepath , " rb " )
lxmf_data = message_file . read ( )
message_file . close ( )
2024-03-02 09:09:51 +01:00
lxm_size = len ( lxmf_data )
next_size = cumulative_size + ( lxm_size + per_message_overhead )
2025-11-01 00:10:30 +01:00
if client_transfer_limit != None and next_size > client_transfer_limit : pass
2024-03-02 09:09:51 +01:00
else :
2025-11-01 00:10:30 +01:00
response_messages . append ( lxmf_data [ : - LXStamper . STAMP_SIZE ] )
2024-03-02 09:09:51 +01:00
cumulative_size + = ( lxm_size + per_message_overhead )
2022-06-17 09:15:56 +02:00
except Exception as e :
RNS . log ( " Error while processing message download request from " + RNS . prettyhexrep ( remote_destination . hash ) + " . The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2025-01-23 14:13:08 +01:00
self . client_propagation_messages_served + = len ( response_messages )
2022-06-17 09:15:56 +02:00
return response_messages
except Exception as e :
RNS . log ( " Error occurred while generating response for download request, the contained exception was: " + str ( e ) , RNS . LOG_DEBUG )
return None
def message_list_response ( self , request_receipt ) :
if request_receipt . response == LXMPeer . ERROR_NO_IDENTITY :
RNS . log ( " Propagation node indicated missing identification on list request, tearing down link. " , RNS . LOG_DEBUG )
if self . outbound_propagation_link != None :
self . outbound_propagation_link . teardown ( )
2022-06-17 13:42:44 +02:00
self . propagation_transfer_state = LXMRouter . PR_NO_IDENTITY_RCVD
elif request_receipt . response == LXMPeer . ERROR_NO_ACCESS :
RNS . log ( " Propagation node did not allow list request, tearing down link. " , RNS . LOG_DEBUG )
if self . outbound_propagation_link != None :
self . outbound_propagation_link . teardown ( )
self . propagation_transfer_state = LXMRouter . PR_NO_ACCESS
2022-06-17 09:15:56 +02:00
else :
2022-06-17 13:42:44 +02:00
if request_receipt . response != None and isinstance ( request_receipt . response , list ) :
2022-06-17 09:15:56 +02:00
haves = [ ]
wants = [ ]
if len ( request_receipt . response ) > 0 :
for transient_id in request_receipt . response :
2024-11-23 13:20:24 +01:00
if self . has_message ( transient_id ) :
if not self . retain_synced_on_node :
haves . append ( transient_id )
2022-06-17 09:15:56 +02:00
else :
if self . propagation_transfer_max_messages == LXMRouter . PR_ALL_MESSAGES or len ( wants ) < self . propagation_transfer_max_messages :
wants . append ( transient_id )
2024-11-23 13:20:24 +01:00
ms = " " if len ( wants ) == 1 else " s "
RNS . log ( f " Requesting { len ( wants ) } message { ms } from propagation node " , RNS . LOG_DEBUG )
2022-06-17 09:15:56 +02:00
request_receipt . link . request (
LXMPeer . MESSAGE_GET_PATH ,
2024-03-02 09:09:51 +01:00
[ wants , haves , self . delivery_per_transfer_limit ] ,
2022-06-17 09:15:56 +02:00
response_callback = self . message_get_response ,
failed_callback = self . message_get_failed ,
2024-11-23 13:20:24 +01:00
progress_callback = self . message_get_progress )
2022-06-17 09:15:56 +02:00
else :
self . propagation_transfer_state = LXMRouter . PR_COMPLETE
self . propagation_transfer_progress = 1.0
self . propagation_transfer_last_result = 0
2024-11-23 13:20:24 +01:00
else :
RNS . log ( " Invalid message list data received from propagation node " , RNS . LOG_DEBUG )
if self . outbound_propagation_link != None :
self . outbound_propagation_link . teardown ( )
2022-06-17 09:15:56 +02:00
def message_get_response ( self , request_receipt ) :
if request_receipt . response == LXMPeer . ERROR_NO_IDENTITY :
RNS . log ( " Propagation node indicated missing identification on get request, tearing down link. " , RNS . LOG_DEBUG )
if self . outbound_propagation_link != None :
self . outbound_propagation_link . teardown ( )
2022-10-22 22:35:07 +02:00
self . propagation_transfer_state = LXMRouter . PR_NO_IDENTITY_RCVD
elif request_receipt . response == LXMPeer . ERROR_NO_ACCESS :
RNS . log ( " Propagation node did not allow get request, tearing down link. " , RNS . LOG_DEBUG )
if self . outbound_propagation_link != None :
self . outbound_propagation_link . teardown ( )
self . propagation_transfer_state = LXMRouter . PR_NO_ACCESS
2022-06-17 09:15:56 +02:00
else :
2025-11-02 02:27:57 +01:00
duplicates = 0
2022-06-17 09:15:56 +02:00
if request_receipt . response != None and len ( request_receipt . response ) > 0 :
haves = [ ]
for lxmf_data in request_receipt . response :
2025-11-02 02:27:57 +01:00
result = self . lxmf_propagation ( lxmf_data , signal_duplicate = LXMRouter . DUPLICATE_SIGNAL )
if result == LXMRouter . DUPLICATE_SIGNAL : duplicates + = 1
2022-06-17 09:15:56 +02:00
haves . append ( RNS . Identity . full_hash ( lxmf_data ) )
# Return a list of successfully received messages to the node.
# This deletes the messages on the propagation node.
# TODO: Add option to keep messages on node.
request_receipt . link . request (
LXMPeer . MESSAGE_GET_PATH ,
[ None , haves ] ,
# response_callback=self.message_syncfinal_response,
failed_callback = self . message_get_failed ,
# progress_callback=self.message_get_progress
)
self . propagation_transfer_state = LXMRouter . PR_COMPLETE
self . propagation_transfer_progress = 1.0
2025-11-02 02:27:57 +01:00
self . propagation_transfer_last_duplicates = duplicates
2022-06-17 09:15:56 +02:00
self . propagation_transfer_last_result = len ( request_receipt . response )
2022-10-22 22:35:07 +02:00
self . save_locally_delivered_transient_ids ( )
2022-06-17 09:15:56 +02:00
def message_get_progress ( self , request_receipt ) :
self . propagation_transfer_state = LXMRouter . PR_RECEIVING
self . propagation_transfer_progress = request_receipt . get_progress ( )
def message_get_failed ( self , request_receipt ) :
RNS . log ( " Message list/get request failed " , RNS . LOG_DEBUG )
if self . outbound_propagation_link != None :
self . outbound_propagation_link . teardown ( )
2023-10-28 00:06:56 +02:00
def acknowledge_sync_completion ( self , reset_state = False , failure_state = None ) :
self . propagation_transfer_last_result = None
2022-06-17 13:42:44 +02:00
if reset_state or self . propagation_transfer_state < = LXMRouter . PR_COMPLETE :
2023-10-28 00:06:56 +02:00
if failure_state == None :
self . propagation_transfer_state = LXMRouter . PR_IDLE
else :
self . propagation_transfer_state = failure_state
2022-06-17 13:42:44 +02:00
2022-06-17 09:15:56 +02:00
self . propagation_transfer_progress = 0.0
self . wants_download_on_path_available_from = None
self . wants_download_on_path_available_to = None
def has_message ( self , transient_id ) :
if transient_id in self . locally_delivered_transient_ids :
return True
else :
return False
2025-10-31 21:45:40 +01:00
def cancel_outbound ( self , message_id , cancel_state = LXMessage . CANCELLED ) :
2025-01-18 19:13:43 +01:00
try :
2025-01-18 20:11:31 +01:00
if message_id in self . pending_deferred_stamps :
lxm = self . pending_deferred_stamps [ message_id ]
RNS . log ( f " Cancelling deferred stamp generation for { lxm } " , RNS . LOG_DEBUG )
2025-10-31 21:45:40 +01:00
lxm . state = cancel_state
2025-01-18 20:11:31 +01:00
LXStamper . cancel_work ( message_id )
2025-01-18 19:13:43 +01:00
lxmessage = None
for lxm in self . pending_outbound :
if lxm . message_id == message_id :
lxmessage = lxm
if lxmessage != None :
2025-10-31 21:45:40 +01:00
lxmessage . state = cancel_state
2025-01-18 19:13:43 +01:00
if lxmessage in self . pending_outbound :
RNS . log ( f " Cancelling { lxmessage } in outbound queue " , RNS . LOG_DEBUG )
if lxmessage . representation == LXMessage . RESOURCE :
if lxmessage . resource_representation != None :
lxmessage . resource_representation . cancel ( )
self . process_outbound ( )
except Exception as e :
RNS . log ( f " An error occurred while cancelling { lxmessage } : { e } " , RNS . LOG_ERROR )
RNS . trace_exception ( e )
2022-06-17 09:15:56 +02:00
def handle_outbound ( self , lxmessage ) :
2024-09-07 15:17:48 +02:00
destination_hash = lxmessage . get_destination ( ) . hash
2024-09-17 13:18:16 +02:00
2024-09-06 20:34:09 +02:00
if lxmessage . stamp_cost == None :
if destination_hash in self . outbound_stamp_costs :
stamp_cost = self . outbound_stamp_costs [ destination_hash ] [ 1 ]
lxmessage . stamp_cost = stamp_cost
RNS . log ( f " No stamp cost set on LXM to { RNS . prettyhexrep ( destination_hash ) } , autoconfigured to { stamp_cost } , as required by latest announce " , RNS . LOG_DEBUG )
2022-06-17 09:15:56 +02:00
lxmessage . state = LXMessage . OUTBOUND
2024-09-07 15:17:48 +02:00
# If an outbound ticket is available for this
# destination, attach it to the message.
lxmessage . outbound_ticket = self . get_outbound_ticket ( destination_hash )
if lxmessage . outbound_ticket != None and lxmessage . defer_stamp :
RNS . log ( f " Deferred stamp generation was requested for { lxmessage } , but outbound ticket was applied, processing immediately " , RNS . LOG_DEBUG )
lxmessage . defer_stamp = False
# If requested, include a ticket to allow the
# destination to reply without generating a stamp.
if lxmessage . include_ticket :
ticket = self . generate_ticket ( lxmessage . destination_hash )
2025-10-31 21:45:40 +01:00
if ticket : lxmessage . fields [ FIELD_TICKET ] = ticket
2024-09-07 15:17:48 +02:00
2025-10-31 21:45:40 +01:00
if not lxmessage . packed : lxmessage . pack ( )
2022-06-17 09:15:56 +02:00
2024-09-17 13:18:16 +02:00
unknown_path_requested = False
if not RNS . Transport . has_path ( destination_hash ) and lxmessage . method == LXMessage . OPPORTUNISTIC :
RNS . log ( f " Pre-emptively requesting unknown path for opportunistic { lxmessage } " , RNS . LOG_DEBUG )
RNS . Transport . request_path ( destination_hash )
2024-09-17 13:29:36 +02:00
lxmessage . next_delivery_attempt = time . time ( ) + LXMRouter . PATH_REQUEST_WAIT
2024-09-17 13:18:16 +02:00
unknown_path_requested = True
2022-06-17 09:15:56 +02:00
2024-09-17 13:18:16 +02:00
lxmessage . determine_transport_encryption ( )
2022-06-17 09:15:56 +02:00
2024-09-07 11:35:17 +02:00
if lxmessage . defer_stamp and lxmessage . stamp_cost == None :
RNS . log ( f " Deferred stamp generation was requested for { lxmessage } , but no stamp is required, processing immediately " , RNS . LOG_DEBUG )
lxmessage . defer_stamp = False
2025-10-31 21:45:40 +01:00
if not lxmessage . defer_stamp and not ( lxmessage . desired_method == LXMessage . PROPAGATED and lxmessage . defer_propagation_stamp ) :
while not unknown_path_requested and self . processing_outbound : time . sleep ( 0.05 )
2024-09-17 13:29:36 +02:00
2024-09-07 22:40:13 +02:00
self . pending_outbound . append ( lxmessage )
2025-10-31 21:45:40 +01:00
if not unknown_path_requested : self . process_outbound ( )
2024-09-17 13:29:36 +02:00
2025-10-31 21:45:40 +01:00
else : self . pending_deferred_stamps [ lxmessage . message_id ] = lxmessage
2022-06-17 09:15:56 +02:00
2024-03-19 11:03:12 +01:00
def get_outbound_progress ( self , lxm_hash ) :
for lxm in self . pending_outbound :
if lxm . hash == lxm_hash :
return lxm . progress
2024-09-07 22:40:13 +02:00
for lxm_id in self . pending_deferred_stamps :
if self . pending_deferred_stamps [ lxm_id ] . hash == lxm_hash :
return self . pending_deferred_stamps [ lxm_id ] . progress
2024-03-19 11:03:12 +01:00
return None
2024-09-07 11:35:17 +02:00
def get_outbound_lxm_stamp_cost ( self , lxm_hash ) :
for lxm in self . pending_outbound :
if lxm . hash == lxm_hash :
2025-11-02 02:27:57 +01:00
if lxm . outbound_ticket : return None
else : return lxm . stamp_cost
2024-09-07 22:40:13 +02:00
for lxm_id in self . pending_deferred_stamps :
if self . pending_deferred_stamps [ lxm_id ] . hash == lxm_hash :
2025-11-02 02:27:57 +01:00
lxm = self . pending_deferred_stamps [ lxm_id ]
if lxm . outbound_ticket : return None
else : return lxm . stamp_cost
2024-09-07 11:35:17 +02:00
return None
2025-10-31 21:45:40 +01:00
def get_outbound_lxm_propagation_stamp_cost ( self , lxm_hash ) :
for lxm in self . pending_outbound :
if lxm . hash == lxm_hash :
return lxm . propagation_target_cost
for lxm_id in self . pending_deferred_stamps :
if self . pending_deferred_stamps [ lxm_id ] . hash == lxm_hash :
2025-11-02 02:27:57 +01:00
return self . pending_deferred_stamps [ lxm_id ] . propagation_target_cost
2025-10-31 21:45:40 +01:00
return None
2024-03-19 11:03:12 +01:00
### Message Routing & Delivery ########################
#######################################################
2025-01-31 13:38:56 +01:00
def lxmf_delivery ( self , lxmf_data , destination_type = None , phy_stats = None , ratchet_id = None , method = None , no_stamp_enforcement = False , allow_duplicate = False ) :
2022-06-17 09:15:56 +02:00
try :
message = LXMessage . unpack_from_bytes ( lxmf_data )
2024-09-08 14:52:54 +02:00
if ratchet_id and not message . ratchet_id :
message . ratchet_id = ratchet_id
if method :
message . method = method
2022-06-17 09:15:56 +02:00
2024-09-07 15:17:48 +02:00
if message . signature_validated and FIELD_TICKET in message . fields :
ticket_entry = message . fields [ FIELD_TICKET ]
if type ( ticket_entry ) == list and len ( ticket_entry ) > 1 :
expires = ticket_entry [ 0 ]
ticket = ticket_entry [ 1 ]
if time . time ( ) < expires :
if type ( ticket ) == bytes and len ( ticket ) == LXMessage . TICKET_LENGTH :
self . remember_ticket ( message . source_hash , ticket_entry )
def save_job ( ) :
self . save_available_tickets ( )
threading . Thread ( target = save_job , daemon = True ) . start ( )
2024-09-06 20:34:09 +02:00
required_stamp_cost = self . delivery_destinations [ message . destination_hash ] . stamp_cost
if required_stamp_cost != None :
2024-09-07 15:17:48 +02:00
destination_tickets = self . get_inbound_tickets ( message . source_hash )
if message . validate_stamp ( required_stamp_cost , tickets = destination_tickets ) :
2024-09-06 20:34:09 +02:00
message . stamp_valid = True
2024-09-08 01:22:00 +02:00
message . stamp_checked = True
2024-09-06 20:34:09 +02:00
else :
message . stamp_valid = False
2024-09-08 01:22:00 +02:00
message . stamp_checked = True
2024-09-06 20:34:09 +02:00
if not message . stamp_valid :
2024-09-08 17:54:11 +02:00
if no_stamp_enforcement :
RNS . log ( f " Received { message } with invalid stamp, but allowing anyway, since stamp enforcement was temporarily disabled " , RNS . LOG_NOTICE )
2024-09-06 20:34:09 +02:00
else :
2024-09-08 17:54:11 +02:00
if self . _enforce_stamps :
RNS . log ( f " Dropping { message } with invalid stamp " , RNS . LOG_NOTICE )
return False
else :
RNS . log ( f " Received { message } with invalid stamp, but allowing anyway, since stamp enforcement is disabled " , RNS . LOG_NOTICE )
else :
2024-09-11 02:09:43 +02:00
RNS . log ( f " Received { message } with valid stamp " , RNS . LOG_DEBUG )
2024-09-06 20:34:09 +02:00
2023-10-23 01:34:20 +02:00
if phy_stats != None :
if " rssi " in phy_stats : message . rssi = phy_stats [ " rssi " ]
if " snr " in phy_stats : message . snr = phy_stats [ " snr " ]
if " q " in phy_stats : message . q = phy_stats [ " q " ]
2024-09-08 14:52:54 +02:00
# TODO: Update these descriptions to account for ratchets
2022-06-17 09:15:56 +02:00
if destination_type == RNS . Destination . SINGLE :
message . transport_encrypted = True
message . transport_encryption = LXMessage . ENCRYPTION_DESCRIPTION_EC
elif destination_type == RNS . Destination . GROUP :
message . transport_encrypted = True
message . transport_encryption = LXMessage . ENCRYPTION_DESCRIPTION_AES
elif destination_type == RNS . Destination . LINK :
message . transport_encrypted = True
message . transport_encryption = LXMessage . ENCRYPTION_DESCRIPTION_EC
else :
message . transport_encrypted = False
message . transport_encryption = None
if message . source_hash in self . ignored_list :
RNS . log ( str ( self ) + " ignored message from " + RNS . prettyhexrep ( message . source_hash ) , RNS . LOG_DEBUG )
return False
2025-01-31 13:38:56 +01:00
if not allow_duplicate and self . has_message ( message . hash ) :
2024-03-19 09:47:37 +01:00
RNS . log ( str ( self ) + " ignored already received message from " + RNS . prettyhexrep ( message . source_hash ) , RNS . LOG_DEBUG )
return False
else :
self . locally_delivered_transient_ids [ message . hash ] = time . time ( )
2022-06-17 09:15:56 +02:00
if self . __delivery_callback != None and callable ( self . __delivery_callback ) :
try :
self . __delivery_callback ( message )
except Exception as e :
RNS . log ( " An error occurred in the external delivery callback for " + str ( message ) , RNS . LOG_ERROR )
2024-09-04 18:57:51 +02:00
RNS . trace_exception ( e )
2022-06-17 09:15:56 +02:00
return True
except Exception as e :
RNS . log ( " Could not assemble LXMF message from received data " , RNS . LOG_NOTICE )
RNS . log ( " The contained exception was: " + str ( e ) , RNS . LOG_DEBUG )
return False
def delivery_packet ( self , data , packet ) :
2024-03-26 14:55:59 +01:00
packet . prove ( )
2022-06-17 09:15:56 +02:00
try :
2024-09-08 14:52:54 +02:00
method = None
2022-06-17 09:15:56 +02:00
if packet . destination_type != RNS . Destination . LINK :
2024-09-08 14:52:54 +02:00
method = LXMessage . OPPORTUNISTIC
2022-06-17 09:15:56 +02:00
lxmf_data = b " "
lxmf_data + = packet . destination . hash
lxmf_data + = data
else :
2024-09-08 14:52:54 +02:00
method = LXMessage . DIRECT
2022-06-17 09:15:56 +02:00
lxmf_data = data
2023-10-28 00:06:56 +02:00
try :
reticulum = RNS . Reticulum . get_instance ( )
if packet . rssi == None : packet . rssi = reticulum . get_packet_rssi ( packet . packet_hash )
if packet . snr == None : packet . snr = reticulum . get_packet_snr ( packet . packet_hash )
if packet . q == None : packet . q = reticulum . get_packet_q ( packet . packet_hash )
except Exception as e :
RNS . log ( " Error while retrieving physical link stats for LXMF delivery packet: " + str ( e ) , RNS . LOG_ERROR )
2023-10-23 01:34:20 +02:00
phy_stats = { " rssi " : packet . rssi , " snr " : packet . snr , " q " : packet . q }
2024-03-22 21:11:32 +01:00
2024-09-08 14:52:54 +02:00
self . lxmf_delivery ( lxmf_data , packet . destination_type , phy_stats = phy_stats , ratchet_id = packet . ratchet_id , method = method )
2022-06-17 09:15:56 +02:00
except Exception as e :
RNS . log ( " Exception occurred while parsing incoming LXMF data. " , RNS . LOG_ERROR )
RNS . log ( " The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
def delivery_link_established ( self , link ) :
2023-10-28 00:06:56 +02:00
link . track_phy_stats ( True )
2022-06-17 09:15:56 +02:00
link . set_packet_callback ( self . delivery_packet )
2024-03-01 23:26:27 +01:00
link . set_resource_strategy ( RNS . Link . ACCEPT_APP )
link . set_resource_callback ( self . delivery_resource_advertised )
2022-06-17 09:15:56 +02:00
link . set_resource_started_callback ( self . resource_transfer_began )
link . set_resource_concluded_callback ( self . delivery_resource_concluded )
2024-09-12 17:57:26 +02:00
link . set_remote_identified_callback ( self . delivery_remote_identified )
2022-06-17 09:15:56 +02:00
def delivery_link_closed ( self , link ) :
pass
def resource_transfer_began ( self , resource ) :
RNS . log ( " Transfer began for LXMF delivery resource " + str ( resource ) , RNS . LOG_DEBUG )
2024-03-01 23:26:27 +01:00
def delivery_resource_advertised ( self , resource ) :
size = resource . get_data_size ( )
limit = self . delivery_per_transfer_limit * 1000
if limit != None and size > limit :
RNS . log ( " Rejecting " + RNS . prettysize ( size ) + " incoming LXMF delivery resource, since it exceeds the limit of " + RNS . prettysize ( limit ) , RNS . LOG_DEBUG )
return False
else :
return True
2022-06-17 09:15:56 +02:00
def delivery_resource_concluded ( self , resource ) :
RNS . log ( " Transfer concluded for LXMF delivery resource " + str ( resource ) , RNS . LOG_DEBUG )
if resource . status == RNS . Resource . COMPLETE :
2024-09-08 14:52:54 +02:00
ratchet_id = None
# Set ratchet ID to link ID if available
if resource . link and hasattr ( resource . link , " link_id " ) :
ratchet_id = resource . link . link_id
2023-10-23 01:34:20 +02:00
phy_stats = { " rssi " : resource . link . rssi , " snr " : resource . link . snr , " q " : resource . link . q }
2024-09-08 14:52:54 +02:00
self . lxmf_delivery ( resource . data . read ( ) , resource . link . type , phy_stats = phy_stats , ratchet_id = ratchet_id , method = LXMessage . DIRECT )
2022-06-17 09:15:56 +02:00
2024-09-12 17:57:26 +02:00
def delivery_remote_identified ( self , link , identity ) :
destination_hash = RNS . Destination . hash_from_name_and_identity ( " lxmf.delivery " , identity )
self . backchannel_links [ destination_hash ] = link
RNS . log ( f " Backchannel became available for { RNS . prettyhexrep ( destination_hash ) } on delivery link { link } " , RNS . LOG_DEBUG )
2022-06-17 09:15:56 +02:00
### Peer Sync & Propagation ###########################
#######################################################
2025-10-31 13:53:59 +01:00
def peer ( self , destination_hash , timestamp , propagation_transfer_limit , propagation_sync_limit , propagation_stamp_cost , propagation_stamp_cost_flexibility , peering_cost , metadata ) :
if peering_cost > self . max_peering_cost :
if destination_hash in self . peers :
RNS . log ( f " Peer { RNS . prettyhexrep ( destination_hash ) } increased peering cost beyond local accepted maximum, breaking peering... " , RNS . LOG_NOTICE )
self . unpeer ( destination_hash , timestamp )
else :
RNS . log ( f " Not peering with { RNS . prettyhexrep ( destination_hash ) } , since its peering cost of { peering_cost } exceeds local maximum of { self . max_peering_cost } " , RNS . LOG_NOTICE )
2022-06-17 09:15:56 +02:00
else :
2025-10-31 13:53:59 +01:00
if destination_hash in self . peers :
peer = self . peers [ destination_hash ]
if timestamp > peer . peering_timebase :
peer . alive = True
2025-10-31 22:24:55 +01:00
peer . metadata = metadata
2025-10-31 13:53:59 +01:00
peer . sync_backoff = 0
peer . next_sync_attempt = 0
peer . peering_timebase = timestamp
peer . last_heard = time . time ( )
peer . propagation_stamp_cost = propagation_stamp_cost
peer . propagation_stamp_cost_flexibility = propagation_stamp_cost_flexibility
peer . peering_cost = peering_cost
peer . propagation_transfer_limit = propagation_transfer_limit
if propagation_sync_limit != None : peer . propagation_sync_limit = propagation_sync_limit
else : peer . propagation_sync_limit = propagation_transfer_limit
RNS . log ( f " Peering config updated for { RNS . prettyhexrep ( destination_hash ) } " , RNS . LOG_VERBOSE )
2025-10-29 23:24:45 +01:00
2025-01-22 01:37:09 +01:00
else :
2025-10-31 13:53:59 +01:00
if len ( self . peers ) > = self . max_peers : RNS . log ( f " Max peers reached, not peering with { RNS . prettyhexrep ( destination_hash ) } " , RNS . LOG_DEBUG )
else :
peer = LXMPeer ( self , destination_hash , sync_strategy = self . default_sync_strategy )
peer . alive = True
2025-10-31 22:24:55 +01:00
peer . metadata = metadata
2025-10-31 13:53:59 +01:00
peer . last_heard = time . time ( )
peer . propagation_stamp_cost = propagation_stamp_cost
peer . propagation_stamp_cost_flexibility = propagation_stamp_cost_flexibility
peer . peering_cost = peering_cost
peer . propagation_transfer_limit = propagation_transfer_limit
if propagation_sync_limit != None : peer . propagation_sync_limit = propagation_sync_limit
else : peer . propagation_sync_limit = propagation_transfer_limit
self . peers [ destination_hash ] = peer
RNS . log ( f " Peered with { RNS . prettyhexrep ( destination_hash ) } " , RNS . LOG_NOTICE )
2022-06-17 08:54:04 +02:00
2022-06-17 09:15:56 +02:00
def unpeer ( self , destination_hash , timestamp = None ) :
if timestamp == None :
timestamp = int ( time . time ( ) )
2022-06-17 08:54:04 +02:00
2022-06-17 09:15:56 +02:00
if destination_hash in self . peers :
peer = self . peers [ destination_hash ]
2022-06-17 08:54:04 +02:00
2022-06-17 09:15:56 +02:00
if timestamp > = peer . peering_timebase :
self . peers . pop ( destination_hash )
RNS . log ( " Broke peering with " + str ( peer . destination ) )
2022-06-17 08:54:04 +02:00
2025-01-29 01:26:36 +01:00
def rotate_peers ( self ) :
try :
rotation_headroom = max ( 1 , math . floor ( self . max_peers * ( LXMRouter . ROTATION_HEADROOM_PCT / 100.0 ) ) )
required_drops = len ( self . peers ) - ( self . max_peers - rotation_headroom )
if required_drops > 0 and len ( self . peers ) - required_drops > 1 :
peers = self . peers . copy ( )
untested_peers = [ ]
for peer_id in self . peers :
peer = self . peers [ peer_id ]
if peer . last_sync_attempt == 0 :
untested_peers . append ( peer )
if len ( untested_peers ) > = rotation_headroom :
RNS . log ( " Newly added peer threshold reached, postponing peer rotation " , RNS . LOG_DEBUG )
return
2025-01-29 14:24:09 +01:00
fully_synced_peers = { }
for peer_id in peers :
peer = peers [ peer_id ]
if peer . unhandled_message_count == 0 :
fully_synced_peers [ peer_id ] = peer
if len ( fully_synced_peers ) > 0 :
peers = fully_synced_peers
ms = " " if len ( fully_synced_peers ) == 1 else " s "
RNS . log ( f " Found { len ( fully_synced_peers ) } fully synced peer { ms } , using as peer rotation pool basis " , RNS . LOG_DEBUG )
2025-01-29 01:26:36 +01:00
culled_peers = [ ]
waiting_peers = [ ]
unresponsive_peers = [ ]
for peer_id in peers :
peer = peers [ peer_id ]
if not peer_id in self . static_peers and peer . state == LXMPeer . IDLE :
if peer . alive :
if peer . offered == 0 :
# Don't consider for unpeering until at
# least one message has been offered
pass
else :
waiting_peers . append ( peer )
else :
unresponsive_peers . append ( peer )
drop_pool = [ ]
if len ( unresponsive_peers ) > 0 :
drop_pool . extend ( unresponsive_peers )
if not self . prioritise_rotating_unreachable_peers :
drop_pool . extend ( waiting_peers )
else :
drop_pool . extend ( waiting_peers )
if len ( drop_pool ) > 0 :
drop_count = min ( required_drops , len ( drop_pool ) )
low_acceptance_rate_peers = sorted (
drop_pool ,
key = lambda p : ( 0 if p . offered == 0 else ( p . outgoing / p . offered ) ) ,
reverse = False
) [ 0 : drop_count ]
2025-01-29 21:21:51 +01:00
dropped_peers = 0
2025-01-29 01:26:36 +01:00
for peer in low_acceptance_rate_peers :
ar = 0 if peer . offered == 0 else round ( ( peer . outgoing / peer . offered ) * 100 , 2 )
2025-01-29 21:21:51 +01:00
if ar < LXMRouter . ROTATION_AR_MAX * 100 :
reachable_str = " reachable " if peer . alive else " unreachable "
RNS . log ( f " Acceptance rate for { reachable_str } peer { RNS . prettyhexrep ( peer . destination_hash ) } was: { ar } % ( { peer . outgoing } / { peer . offered } , { peer . unhandled_message_count } unhandled messages) " , RNS . LOG_DEBUG )
self . unpeer ( peer . destination_hash )
2025-01-30 00:37:50 +01:00
dropped_peers + = 1
2025-01-29 21:21:51 +01:00
ms = " " if dropped_peers == 1 else " s "
RNS . log ( f " Dropped { dropped_peers } low acceptance rate peer { ms } to increase peering headroom " , RNS . LOG_DEBUG )
2025-01-29 01:26:36 +01:00
except Exception as e :
RNS . log ( f " An error occurred during peer rotation: { e } " , RNS . LOG_ERROR )
RNS . trace_exception ( e )
2022-06-17 09:15:56 +02:00
def sync_peers ( self ) :
culled_peers = [ ]
waiting_peers = [ ]
2022-12-20 23:58:09 +01:00
unresponsive_peers = [ ]
2025-01-21 20:20:39 +01:00
peers = self . peers . copy ( )
for peer_id in peers :
peer = peers [ peer_id ]
2022-06-17 09:15:56 +02:00
if time . time ( ) > peer . last_heard + LXMPeer . MAX_UNREACHABLE :
2025-10-29 23:24:45 +01:00
if not peer_id in self . static_peers : culled_peers . append ( peer_id )
2022-06-17 09:15:56 +02:00
else :
if peer . state == LXMPeer . IDLE and len ( peer . unhandled_messages ) > 0 :
2025-10-29 23:24:45 +01:00
if peer . alive : waiting_peers . append ( peer )
2022-12-20 23:58:09 +01:00
else :
2025-10-29 23:24:45 +01:00
if hasattr ( peer , " next_sync_attempt " ) and time . time ( ) > peer . next_sync_attempt : unresponsive_peers . append ( peer )
else : pass # RNS.log("Not adding peer "+str(peer)+" since it is in sync backoff", RNS.LOG_DEBUG)
2022-06-17 08:54:04 +02:00
2022-12-20 23:58:09 +01:00
peer_pool = [ ]
2022-06-17 09:15:56 +02:00
if len ( waiting_peers ) > 0 :
2023-02-17 17:10:31 +01:00
fastest_peers = sorted (
waiting_peers ,
2025-01-13 14:37:51 +01:00
key = lambda p : p . sync_transfer_rate ,
2023-02-17 17:10:31 +01:00
reverse = True
2023-02-17 19:36:20 +01:00
) [ 0 : min ( LXMRouter . FASTEST_N_RANDOM_POOL , len ( waiting_peers ) ) ]
2023-02-17 17:10:31 +01:00
peer_pool . extend ( fastest_peers )
2025-01-13 14:37:51 +01:00
unknown_speed_peers = [ p for p in waiting_peers if p . sync_transfer_rate == 0 ]
2023-02-17 17:10:31 +01:00
if len ( unknown_speed_peers ) > 0 :
peer_pool . extend (
unknown_speed_peers [
0 : min (
len ( unknown_speed_peers ) ,
len ( fastest_peers )
) ]
)
RNS . log ( " Selecting peer to sync from " + str ( len ( waiting_peers ) ) + " waiting peers. " , RNS . LOG_DEBUG )
2022-12-20 23:58:09 +01:00
elif len ( unresponsive_peers ) > 0 :
RNS . log ( " No active peers available, randomly selecting peer to sync from " + str ( len ( unresponsive_peers ) ) + " unresponsive peers. " , RNS . LOG_DEBUG )
peer_pool = unresponsive_peers
if len ( peer_pool ) > 0 :
selected_index = random . randint ( 0 , len ( peer_pool ) - 1 )
selected_peer = peer_pool [ selected_index ]
2022-06-17 09:15:56 +02:00
RNS . log ( " Selected waiting peer " + str ( selected_index ) + " : " + RNS . prettyhexrep ( selected_peer . destination . hash ) , RNS . LOG_DEBUG )
selected_peer . sync ( )
2022-06-17 08:54:04 +02:00
2022-06-17 09:15:56 +02:00
for peer_id in culled_peers :
RNS . log ( " Removing peer " + RNS . prettyhexrep ( peer_id ) + " due to excessive unreachability " , RNS . LOG_WARNING )
try :
if peer_id in self . peers :
self . peers . pop ( peer_id )
2022-06-17 08:54:04 +02:00
except Exception as e :
2022-06-17 09:15:56 +02:00
RNS . log ( " Error while removing peer " + RNS . prettyhexrep ( peer_id ) + " . The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2022-06-17 08:54:04 +02:00
def propagation_link_established ( self , link ) :
link . set_packet_callback ( self . propagation_packet )
2024-03-01 23:26:27 +01:00
link . set_resource_strategy ( RNS . Link . ACCEPT_APP )
link . set_resource_callback ( self . propagation_resource_advertised )
2022-06-17 08:54:04 +02:00
link . set_resource_started_callback ( self . resource_transfer_began )
link . set_resource_concluded_callback ( self . propagation_resource_concluded )
2024-02-29 23:02:16 +01:00
self . active_propagation_links . append ( link )
2022-06-17 08:54:04 +02:00
2024-03-01 23:26:27 +01:00
def propagation_resource_advertised ( self , resource ) :
2025-01-22 01:37:09 +01:00
if self . from_static_only :
remote_identity = resource . link . get_remote_identity ( )
if remote_identity == None :
RNS . log ( f " Rejecting propagation resource from unidentified peer " , RNS . LOG_DEBUG )
return False
else :
remote_destination = RNS . Destination ( remote_identity , RNS . Destination . OUT , RNS . Destination . SINGLE , APP_NAME , " propagation " )
remote_hash = remote_destination . hash
remote_str = RNS . prettyhexrep ( remote_hash )
if not remote_hash in self . static_peers :
RNS . log ( f " Rejecting propagation resource from { remote_str } not in static peers list " , RNS . LOG_DEBUG )
return False
2024-03-01 23:26:27 +01:00
size = resource . get_data_size ( )
2025-10-29 23:24:45 +01:00
limit = self . propagation_per_sync_limit * 1000
2024-03-01 23:26:27 +01:00
if limit != None and size > limit :
2025-01-22 01:37:09 +01:00
RNS . log ( f " Rejecting { RNS . prettysize ( size ) } incoming propagation resource, since it exceeds the limit of { RNS . prettysize ( limit ) } " , RNS . LOG_DEBUG )
2024-03-01 23:26:27 +01:00
return False
else :
return True
2022-06-17 08:54:04 +02:00
def propagation_packet ( self , data , packet ) :
try :
2025-10-30 21:19:38 +01:00
if packet . destination_type != RNS . Destination . LINK : return
2022-06-17 08:54:04 +02:00
else :
2025-10-31 17:11:17 +01:00
data = msgpack . unpackb ( data )
remote_timebase = data [ 0 ]
messages = data [ 1 ]
min_accepted_cost = max ( 0 , self . propagation_stamp_cost - self . propagation_stamp_cost_flexibility )
2025-10-31 13:53:59 +01:00
validated_messages = LXStamper . validate_pn_stamps ( messages , min_accepted_cost )
2022-06-17 08:54:04 +02:00
2025-10-31 02:19:24 +01:00
for validated_entry in validated_messages :
lxmf_data = validated_entry [ 1 ]
stamp_value = validated_entry [ 2 ]
2025-11-01 00:10:30 +01:00
stamp_data = validated_entry [ 3 ]
self . lxmf_propagation ( lxmf_data , stamp_value = stamp_value , stamp_data = stamp_data )
2025-01-23 14:13:08 +01:00
self . client_propagation_messages_received + = 1
2022-06-17 08:54:04 +02:00
2025-11-02 18:35:15 +01:00
if len ( validated_messages ) == len ( messages ) :
ms = " " if len ( messages ) == 1 else " s "
RNS . log ( f " Received { len ( messages ) } propagation message { ms } from client with valid stamp { ms } " , RNS . LOG_DEBUG )
packet . prove ( )
2025-10-31 21:45:40 +01:00
else :
RNS . log ( " Propagation transfer from client contained messages with invalid stamps " , RNS . LOG_NOTICE )
reject_data = msgpack . packb ( [ LXMPeer . ERROR_INVALID_STAMP ] )
RNS . Packet ( packet . link , reject_data ) . send ( )
packet . link . teardown ( )
2022-06-17 08:54:04 +02:00
except Exception as e :
RNS . log ( " Exception occurred while parsing incoming LXMF propagation data. " , RNS . LOG_ERROR )
RNS . log ( " The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2024-02-29 23:02:16 +01:00
def offer_request ( self , path , data , request_id , link_id , remote_identity , requested_at ) :
2022-06-17 09:15:56 +02:00
if remote_identity == None :
return LXMPeer . ERROR_NO_IDENTITY
else :
2025-11-01 01:35:15 +01:00
remote_destination = RNS . Destination ( remote_identity , RNS . Destination . OUT , RNS . Destination . SINGLE , APP_NAME , " propagation " )
remote_hash = remote_destination . hash
remote_str = RNS . prettyhexrep ( remote_hash )
if remote_hash in self . throttled_peers :
throttle_remaining = self . throttled_peers [ remote_hash ] - time . time ( )
if throttle_remaining > 0 :
RNS . log ( f " Propagation offer from node { remote_str } rejected, throttled for { RNS . prettytime ( throttle_remaining ) } more " , RNS . LOG_NOTICE )
return LXMPeer . ERROR_THROTTLED
else : self . throttled_peers . pop ( remote_hash )
2025-01-22 01:37:09 +01:00
if self . from_static_only :
if not remote_hash in self . static_peers :
RNS . log ( f " Rejecting propagation request from { remote_str } not in static peers list " , RNS . LOG_DEBUG )
return LXMPeer . ERROR_NO_ACCESS
2022-06-17 09:15:56 +02:00
try :
2025-10-31 17:02:34 +01:00
if type ( data ) != list and len ( data ) < 2 : return LXMPeer . ERROR_INVALID_DATA
2022-06-17 09:15:56 +02:00
2025-11-01 00:10:30 +01:00
peering_id = self . identity . hash + remote_identity . hash
2025-10-31 17:02:34 +01:00
target_cost = self . peering_cost
peering_key = data [ 0 ]
transient_ids = data [ 1 ]
wanted_ids = [ ]
2022-06-17 09:15:56 +02:00
2025-10-31 17:02:34 +01:00
ts = time . time ( )
peering_key_valid = LXStamper . validate_peering_key ( peering_id , peering_key , target_cost )
td = time . time ( ) - ts
2022-06-17 09:15:56 +02:00
2025-10-31 17:02:34 +01:00
if not peering_key_valid :
RNS . log ( f " Invalid peering key for incoming sync offer " , RNS . LOG_DEBUG )
return LXMPeer . ERROR_INVALID_KEY
2022-06-17 09:15:56 +02:00
else :
2025-10-31 17:02:34 +01:00
RNS . log ( f " Peering key validated for incoming offer in { RNS . prettytime ( td ) } " , RNS . LOG_DEBUG )
2025-11-02 18:35:15 +01:00
self . validated_peer_links [ link_id ] = True
2025-10-31 17:02:34 +01:00
for transient_id in transient_ids :
if not transient_id in self . propagation_entries : wanted_ids . append ( transient_id )
if len ( wanted_ids ) == 0 : return False
elif len ( wanted_ids ) == len ( transient_ids ) : return True
else : return wanted_ids
2022-06-17 09:15:56 +02:00
except Exception as e :
RNS . log ( " Error occurred while generating response for sync request, the contained exception was: " + str ( e ) , RNS . LOG_DEBUG )
2025-11-01 00:10:30 +01:00
RNS . trace_exception ( e )
2022-06-17 09:15:56 +02:00
return None
2022-06-17 08:54:04 +02:00
def propagation_resource_concluded ( self , resource ) :
if resource . status == RNS . Resource . COMPLETE :
try :
data = msgpack . unpackb ( resource . data . read ( ) )
if type ( data ) == list and len ( data ) == 2 and type ( data [ 0 ] == float ) and type ( data [ 1 ] ) == list :
# This is a series of propagation messages from a peer or originator
remote_identity = resource . link . get_remote_identity ( )
2025-10-30 21:19:38 +01:00
remote_timebase = data [ 0 ]
messages = data [ 1 ]
remote_hash = None
2025-11-02 18:35:15 +01:00
remote_str = " unknown client "
2022-06-17 08:54:04 +02:00
if remote_identity != None :
remote_destination = RNS . Destination ( remote_identity , RNS . Destination . OUT , RNS . Destination . SINGLE , APP_NAME , " propagation " )
2025-10-30 21:19:38 +01:00
remote_hash = remote_destination . hash
remote_app_data = RNS . Identity . recall_app_data ( remote_hash )
remote_str = RNS . prettyhexrep ( remote_hash )
if remote_hash in self . peers : remote_str = f " peer { remote_str } "
2025-01-22 01:37:09 +01:00
else :
2025-10-30 21:19:38 +01:00
if pn_announce_data_is_valid ( remote_app_data ) :
# 1: Current node timebase
# 2: Boolean flag signalling propagation node state
# 3: Per-transfer limit for message propagation in kilobytes
# 4: Limit for incoming propagation node syncs
2025-10-31 13:53:59 +01:00
# 5: Propagation stamp costs for this node
2025-10-30 21:19:38 +01:00
# 6: Node metadata
if remote_app_data [ 2 ] and self . autopeer and RNS . Transport . hops_to ( remote_hash ) < = self . autopeer_maxdepth :
remote_timebase = remote_app_data [ 1 ]
remote_transfer_limit = remote_app_data [ 3 ]
remote_sync_limit = remote_app_data [ 4 ]
remote_stamp_cost = remote_app_data [ 5 ] [ 0 ]
remote_stamp_flex = remote_app_data [ 5 ] [ 1 ]
2025-10-31 13:53:59 +01:00
remote_peering_cost = remote_app_data [ 5 ] [ 2 ]
2025-10-30 21:19:38 +01:00
remote_metadata = remote_app_data [ 6 ]
RNS . log ( f " Auto-peering with { remote_str } discovered via incoming sync " , RNS . LOG_DEBUG ) # TODO: Remove debug
2025-10-31 13:53:59 +01:00
self . peer ( remote_hash , remote_timebase , remote_transfer_limit , remote_sync_limit , remote_stamp_cost , remote_stamp_flex , remote_peering_cost , remote_metadata )
2022-06-17 08:54:04 +02:00
2025-11-02 18:35:15 +01:00
peering_key_valid = False
if remote_identity != None :
if resource . link . link_id in self . validated_peer_links and self . validated_peer_links [ resource . link . link_id ] == True :
peering_key_valid = True
2025-01-21 16:33:39 +01:00
2025-11-02 18:35:15 +01:00
if not peering_key_valid and len ( messages ) > 1 :
2025-11-01 01:35:15 +01:00
resource . link . teardown ( )
2025-11-02 18:35:15 +01:00
RNS . log ( f " Received multiple propagation messages from { remote_str } without valid peering key presentation. This is not supposed to happen, ignoring. " , RNS . LOG_WARNING )
RNS . log ( f " Clients and peers without a valid peering key can only deliver 1 message per transfer. " , RNS . LOG_WARNING )
else :
ms = " " if len ( messages ) == 1 else " s "
RNS . log ( f " Received { len ( messages ) } message { ms } from { remote_str } , validating stamps... " , RNS . LOG_VERBOSE )
min_accepted_cost = max ( 0 , self . propagation_stamp_cost - self . propagation_stamp_cost_flexibility )
validated_messages = LXStamper . validate_pn_stamps ( messages , min_accepted_cost )
invalid_stamps = len ( messages ) - len ( validated_messages )
ms = " " if invalid_stamps == 1 else " s "
if len ( validated_messages ) == len ( messages ) : RNS . log ( f " All message stamps validated from { remote_str } " , RNS . LOG_VERBOSE )
else : RNS . log ( f " Transfer from { remote_str } contained { invalid_stamps } invalid stamp { ms } " , RNS . LOG_WARNING )
for validated_entry in validated_messages :
transient_id = validated_entry [ 0 ]
lxmf_data = validated_entry [ 1 ]
stamp_value = validated_entry [ 2 ]
stamp_data = validated_entry [ 3 ]
peer = None
if remote_hash != None and remote_hash in self . peers :
peer = self . peers [ remote_hash ]
peer . incoming + = 1
peer . rx_bytes + = len ( lxmf_data )
else :
if remote_identity != None :
self . unpeered_propagation_incoming + = 1
self . unpeered_propagation_rx_bytes + = len ( lxmf_data )
else :
self . client_propagation_messages_received + = 1
self . lxmf_propagation ( lxmf_data , from_peer = peer , stamp_value = stamp_value , stamp_data = stamp_data )
if peer != None : peer . queue_handled_message ( transient_id )
invalid_message_count = len ( messages ) - len ( validated_messages )
if invalid_message_count > 0 :
resource . link . teardown ( )
if remote_identity != None :
throttle_time = LXMRouter . PN_STAMP_THROTTLE
self . throttled_peers [ remote_hash ] = time . time ( ) + throttle_time
ms = " " if invalid_message_count == 1 else " s "
RNS . log ( f " Propagation transfer from { remote_str } contained { invalid_message_count } message { ms } with invalid stamps, throttled for { RNS . prettytime ( throttle_time ) } " , RNS . LOG_NOTICE )
2025-11-01 01:35:15 +01:00
2022-06-17 08:54:04 +02:00
else :
RNS . log ( " Invalid data structure received at propagation destination, ignoring " , RNS . LOG_DEBUG )
except Exception as e :
RNS . log ( " Error while unpacking received propagation resource " , RNS . LOG_DEBUG )
2025-01-21 16:33:39 +01:00
RNS . trace_exception ( e )
2022-06-17 08:54:04 +02:00
2025-01-21 20:20:39 +01:00
def enqueue_peer_distribution ( self , transient_id , from_peer ) :
self . peer_distribution_queue . append ( [ transient_id , from_peer ] )
def flush_peer_distribution_queue ( self ) :
if len ( self . peer_distribution_queue ) > 0 :
entries = [ ]
while len ( self . peer_distribution_queue ) > 0 :
entries . append ( self . peer_distribution_queue . pop ( ) )
for peer_id in self . peers . copy ( ) :
if peer_id in self . peers :
peer = self . peers [ peer_id ]
for entry in entries :
transient_id = entry [ 0 ]
from_peer = entry [ 1 ]
if peer != from_peer :
peer . queue_unhandled_message ( transient_id )
2025-11-01 00:10:30 +01:00
def lxmf_propagation ( self , lxmf_data , signal_local_delivery = None , signal_duplicate = None , allow_duplicate = False , is_paper_message = False ,
from_peer = None , stamp_value = None , stamp_data = None ) :
2025-10-30 21:19:38 +01:00
if is_paper_message : no_stamp_enforcement = True
else : no_stamp_enforcement = False
2024-09-08 17:54:11 +02:00
2022-06-17 08:54:04 +02:00
try :
if len ( lxmf_data ) > = LXMessage . LXMF_OVERHEAD :
transient_id = RNS . Identity . full_hash ( lxmf_data )
2022-11-19 20:07:00 +01:00
2025-01-31 13:38:56 +01:00
if ( not transient_id in self . propagation_entries and not transient_id in self . locally_processed_transient_ids ) or allow_duplicate == True :
2022-06-17 08:54:04 +02:00
received = time . time ( )
destination_hash = lxmf_data [ : LXMessage . DESTINATION_LENGTH ]
2022-11-19 20:07:00 +01:00
self . locally_processed_transient_ids [ transient_id ] = received
2022-06-17 08:54:04 +02:00
if destination_hash in self . delivery_destinations :
delivery_destination = self . delivery_destinations [ destination_hash ]
encrypted_lxmf_data = lxmf_data [ LXMessage . DESTINATION_LENGTH : ]
decrypted_lxmf_data = delivery_destination . decrypt ( encrypted_lxmf_data )
2024-09-05 14:56:49 +02:00
if decrypted_lxmf_data != None :
delivery_data = lxmf_data [ : LXMessage . DESTINATION_LENGTH ] + decrypted_lxmf_data
2025-01-31 13:38:56 +01:00
self . lxmf_delivery ( delivery_data , delivery_destination . type , ratchet_id = delivery_destination . latest_ratchet_id , method = LXMessage . PROPAGATED , no_stamp_enforcement = no_stamp_enforcement , allow_duplicate = allow_duplicate )
2024-09-05 14:56:49 +02:00
self . locally_delivered_transient_ids [ transient_id ] = time . time ( )
2022-06-17 08:54:04 +02:00
2024-09-05 14:56:49 +02:00
if signal_local_delivery != None :
return signal_local_delivery
2022-11-19 20:07:00 +01:00
2022-06-17 08:54:04 +02:00
else :
if self . propagation_node :
2025-11-01 00:10:30 +01:00
stamped_data = lxmf_data + stamp_data
2025-10-30 21:19:38 +01:00
value_component = f " _ { stamp_value } " if stamp_value and stamp_value > 0 else " "
file_path = f " { self . messagepath } / { RNS . hexrep ( transient_id , delimit = False ) } _ { received } { value_component } "
msg_file = open ( file_path , " wb " )
2025-11-01 00:10:30 +01:00
msg_file . write ( stamped_data ) ; msg_file . close ( )
2022-06-17 08:54:04 +02:00
2025-10-31 13:53:59 +01:00
RNS . log ( f " Received propagated LXMF message { RNS . prettyhexrep ( transient_id ) } with stamp value { stamp_value } , adding to peer distribution queues... " , RNS . LOG_EXTREME )
2025-11-01 00:10:30 +01:00
self . propagation_entries [ transient_id ] = [ destination_hash , file_path , time . time ( ) , len ( stamped_data ) , [ ] , [ ] , stamp_value ]
2025-01-21 20:20:39 +01:00
self . enqueue_peer_distribution ( transient_id , from_peer )
2022-06-17 08:54:04 +02:00
2022-11-19 20:07:00 +01:00
else :
# TODO: Add message to sneakernet queues when implemented
2025-10-31 13:53:59 +01:00
RNS . log ( f " Received propagated LXMF message { RNS . prettyhexrep ( transient_id ) } , but this instance is not hosting a propagation node, discarding message. " , RNS . LOG_DEBUG )
2022-11-19 20:07:00 +01:00
2022-06-17 08:54:04 +02:00
return True
2022-11-19 20:07:00 +01:00
else :
if signal_duplicate != None :
return signal_duplicate
else :
return False
2022-06-17 08:54:04 +02:00
return False
except Exception as e :
RNS . log ( " Could not assemble propagated LXMF message from received data " , RNS . LOG_DEBUG )
RNS . log ( " The contained exception was: " + str ( e ) , RNS . LOG_DEBUG )
2025-01-21 16:33:39 +01:00
RNS . trace_exception ( e )
2022-06-17 08:54:04 +02:00
return False
2025-01-31 13:38:56 +01:00
def ingest_lxm_uri ( self , uri , signal_local_delivery = None , signal_duplicate = None , allow_duplicate = False ) :
2022-11-19 20:07:00 +01:00
try :
2022-11-22 19:53:25 +01:00
if not uri . lower ( ) . startswith ( LXMessage . URI_SCHEMA + " :// " ) :
RNS . log ( " Cannot ingest LXM, invalid URI provided. " , RNS . LOG_ERROR )
2022-11-19 20:07:00 +01:00
return False
else :
2022-11-22 19:53:25 +01:00
lxmf_data = base64 . urlsafe_b64decode ( uri . replace ( LXMessage . URI_SCHEMA + " :// " , " " ) . replace ( " / " , " " ) + " == " )
2022-11-19 20:07:00 +01:00
transient_id = RNS . Identity . full_hash ( lxmf_data )
2025-01-31 13:38:56 +01:00
router_propagation_result = self . lxmf_propagation ( lxmf_data , signal_local_delivery = signal_local_delivery , signal_duplicate = signal_duplicate , allow_duplicate = allow_duplicate , is_paper_message = True )
2022-11-19 20:07:00 +01:00
if router_propagation_result != False :
RNS . log ( " LXM with transient ID " + RNS . prettyhexrep ( transient_id ) + " was ingested. " , RNS . LOG_DEBUG )
return router_propagation_result
else :
2022-11-22 19:53:25 +01:00
RNS . log ( " No valid LXM could be ingested from the provided URI " , RNS . LOG_DEBUG )
2022-11-19 20:07:00 +01:00
return False
except Exception as e :
2022-11-22 19:53:25 +01:00
RNS . log ( " Error while decoding URI-encoded LXMF message. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2022-11-19 20:07:00 +01:00
return False
2022-06-17 08:54:04 +02:00
def fail_message ( self , lxmessage ) :
RNS . log ( str ( lxmessage ) + " failed to send " , RNS . LOG_DEBUG )
2024-09-07 22:40:13 +02:00
if lxmessage in self . pending_outbound :
self . pending_outbound . remove ( lxmessage )
2022-06-17 08:54:04 +02:00
self . failed_outbound . append ( lxmessage )
2025-01-18 21:36:08 +01:00
if lxmessage . state != LXMessage . REJECTED :
lxmessage . state = LXMessage . FAILED
2022-06-17 08:54:04 +02:00
if lxmessage . failed_callback != None and callable ( lxmessage . failed_callback ) :
lxmessage . failed_callback ( lxmessage )
2024-09-07 22:40:13 +02:00
def process_deferred_stamps ( self ) :
if len ( self . pending_deferred_stamps ) > 0 :
if self . stamp_gen_lock . locked ( ) :
return
else :
with self . stamp_gen_lock :
selected_lxm = None
selected_message_id = None
for message_id in self . pending_deferred_stamps :
lxmessage = self . pending_deferred_stamps [ message_id ]
if selected_lxm == None :
selected_lxm = lxmessage
selected_message_id = message_id
if selected_lxm != None :
2025-01-18 20:11:31 +01:00
if selected_lxm . state == LXMessage . CANCELLED :
RNS . log ( f " Message cancelled during deferred stamp generation for { selected_lxm } . " , RNS . LOG_DEBUG )
selected_lxm . stamp_generation_failed = True
self . pending_deferred_stamps . pop ( selected_message_id )
if selected_lxm . failed_callback != None and callable ( selected_lxm . failed_callback ) :
selected_lxm . failed_callback ( lxmessage )
return
2025-11-01 00:10:30 +01:00
if selected_lxm . defer_stamp :
if selected_lxm . stamp == None : stamp_generation_success = False
else : stamp_generation_success = True
else : stamp_generation_success = True
2025-10-31 21:45:40 +01:00
if selected_lxm . desired_method == LXMessage . PROPAGATED :
if selected_lxm . propagation_stamp == None : propagation_stamp_generation_success = False
else : propagation_stamp_generation_success = True
else : propagation_stamp_generation_success = True
if stamp_generation_success == False :
RNS . log ( f " Starting stamp generation for { selected_lxm } ... " , RNS . LOG_DEBUG )
generated_stamp = selected_lxm . get_stamp ( )
if generated_stamp :
selected_lxm . stamp = generated_stamp
selected_lxm . defer_stamp = False
selected_lxm . packed = None
selected_lxm . pack ( )
stamp_generation_success = True
RNS . log ( f " Stamp generation completed for { selected_lxm } " , RNS . LOG_DEBUG )
2025-01-18 19:13:43 +01:00
else :
2025-10-31 21:45:40 +01:00
if selected_lxm . state == LXMessage . CANCELLED :
RNS . log ( f " Message cancelled during deferred stamp generation for { selected_lxm } . " , RNS . LOG_DEBUG )
selected_lxm . stamp_generation_failed = True
self . pending_deferred_stamps . pop ( selected_message_id )
if selected_lxm . failed_callback != None and callable ( selected_lxm . failed_callback ) :
selected_lxm . failed_callback ( lxmessage )
else :
RNS . log ( f " Deferred stamp generation did not succeed. Failing { selected_lxm } . " , RNS . LOG_ERROR )
selected_lxm . stamp_generation_failed = True
self . pending_deferred_stamps . pop ( selected_message_id )
self . fail_message ( selected_lxm )
if propagation_stamp_generation_success == False :
RNS . log ( f " Starting propagation stamp generation for { selected_lxm } ... " , RNS . LOG_DEBUG )
pn_target_cost = self . get_outbound_propagation_cost ( )
if pn_target_cost == None :
RNS . log ( " Failed to get propagation node stamp cost, cannot generate propagation stamp " , RNS . LOG_ERROR )
2025-01-18 19:13:43 +01:00
selected_lxm . stamp_generation_failed = True
self . pending_deferred_stamps . pop ( selected_message_id )
self . fail_message ( selected_lxm )
2024-09-07 22:40:13 +02:00
2025-10-31 21:45:40 +01:00
else :
propagation_stamp = selected_lxm . get_propagation_stamp ( target_cost = pn_target_cost )
if propagation_stamp :
selected_lxm . propagation_stamp = propagation_stamp
selected_lxm . defer_propagation_stamp = False
selected_lxm . packed = None
selected_lxm . pack ( )
propagation_stamp_generation_success = True
RNS . log ( f " Propagation stamp generation completed for { selected_lxm } " , RNS . LOG_DEBUG )
else :
if selected_lxm . state == LXMessage . CANCELLED :
RNS . log ( f " Message cancelled during deferred propagation stamp generation for { selected_lxm } . " , RNS . LOG_DEBUG )
selected_lxm . stamp_generation_failed = True
self . pending_deferred_stamps . pop ( selected_message_id )
if selected_lxm . failed_callback != None and callable ( selected_lxm . failed_callback ) :
selected_lxm . failed_callback ( lxmessage )
else :
RNS . log ( f " Deferred propagation stamp generation did not succeed. Failing { selected_lxm } . " , RNS . LOG_ERROR )
selected_lxm . stamp_generation_failed = True
self . pending_deferred_stamps . pop ( selected_message_id )
self . fail_message ( selected_lxm )
if stamp_generation_success and propagation_stamp_generation_success :
self . pending_deferred_stamps . pop ( selected_message_id )
self . pending_outbound . append ( selected_lxm )
def propagation_transfer_signalling_packet ( self , data , packet ) :
try :
unpacked = msgpack . unpackb ( data )
if type ( unpacked ) == list and len ( unpacked ) > = 1 :
signal = unpacked [ 0 ]
if signal == LXMPeer . ERROR_INVALID_STAMP :
RNS . log ( " Message rejected by propagation node " , RNS . LOG_ERROR )
if hasattr ( packet , " link " ) and hasattr ( packet . link , " for_lxmessage " ) :
lxm = packet . link . for_lxmessage
RNS . log ( f " Invalid propagation stamp on { lxm } " , RNS . LOG_ERROR )
self . cancel_outbound ( lxm . message_id , cancel_state = LXMessage . REJECTED )
except Exception as e :
RNS . log ( f " An error occurred while processing propagation transfer signalling. The contained exception was: { e } " , RNS . LOG_ERROR )
2024-09-07 22:40:13 +02:00
2022-06-17 08:54:04 +02:00
def process_outbound ( self , sender = None ) :
if self . processing_outbound :
return
for lxmessage in self . pending_outbound :
if lxmessage . state == LXMessage . DELIVERED :
RNS . log ( " Delivery has occurred for " + str ( lxmessage ) + " , removing from outbound queue " , RNS . LOG_DEBUG )
self . pending_outbound . remove ( lxmessage )
2024-09-12 17:57:26 +02:00
# Udate ticket delivery stats
2024-09-07 15:17:48 +02:00
if lxmessage . include_ticket and FIELD_TICKET in lxmessage . fields :
RNS . log ( f " Updating latest ticket delivery for { RNS . prettyhexrep ( lxmessage . destination_hash ) } " , RNS . LOG_DEBUG )
self . available_tickets [ " last_deliveries " ] [ lxmessage . destination_hash ] = time . time ( )
self . save_available_tickets ( )
2024-09-12 17:57:26 +02:00
# Prepare link for backchannel communications
delivery_destination_hash = lxmessage . get_destination ( ) . hash
2024-09-16 20:08:26 +02:00
if lxmessage . method == LXMessage . DIRECT and delivery_destination_hash in self . direct_links :
2024-09-12 17:57:26 +02:00
direct_link = self . direct_links [ delivery_destination_hash ]
if not hasattr ( direct_link , " backchannel_identified " ) or direct_link . backchannel_identified == False :
if direct_link . initiator == True :
source_destination_hash = lxmessage . get_source ( ) . hash
if source_destination_hash in self . delivery_destinations :
backchannel_identity = self . delivery_destinations [ source_destination_hash ] . identity
2024-09-12 18:24:14 +02:00
backchannel_desthash = RNS . Destination . hash_from_name_and_identity ( " lxmf.delivery " , backchannel_identity )
2024-09-12 17:57:26 +02:00
direct_link . identify ( backchannel_identity )
2024-09-12 21:28:50 +02:00
direct_link . backchannel_identified = True
2024-09-12 17:57:26 +02:00
self . delivery_link_established ( direct_link )
2024-09-12 18:24:14 +02:00
RNS . log ( f " Performed backchannel identification as { RNS . prettyhexrep ( backchannel_desthash ) } on { direct_link } " , RNS . LOG_DEBUG )
2024-09-12 17:57:26 +02:00
2024-09-05 14:56:49 +02:00
elif lxmessage . method == LXMessage . PROPAGATED and lxmessage . state == LXMessage . SENT :
2022-06-17 08:54:04 +02:00
RNS . log ( " Propagation has occurred for " + str ( lxmessage ) + " , removing from outbound queue " , RNS . LOG_DEBUG )
self . pending_outbound . remove ( lxmessage )
2024-09-17 13:29:36 +02:00
2025-01-18 19:13:43 +01:00
elif lxmessage . state == LXMessage . CANCELLED :
RNS . log ( " Cancellation requested for " + str ( lxmessage ) + " , removing from outbound queue " , RNS . LOG_DEBUG )
self . pending_outbound . remove ( lxmessage )
if lxmessage . failed_callback != None and callable ( lxmessage . failed_callback ) :
lxmessage . failed_callback ( lxmessage )
2025-01-18 21:36:08 +01:00
elif lxmessage . state == LXMessage . REJECTED :
RNS . log ( " Receiver rejected " + str ( lxmessage ) + " , removing from outbound queue " , RNS . LOG_DEBUG )
2025-10-31 21:45:40 +01:00
if lxmessage in self . pending_outbound : self . pending_outbound . remove ( lxmessage )
2025-01-18 21:36:08 +01:00
if lxmessage . failed_callback != None and callable ( lxmessage . failed_callback ) :
lxmessage . failed_callback ( lxmessage )
2022-06-17 08:54:04 +02:00
else :
2025-01-18 19:13:43 +01:00
RNS . log ( " Outbound processing for " + str ( lxmessage ) + " to " + RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) , RNS . LOG_DEBUG )
2024-09-07 11:35:17 +02:00
2025-04-17 13:31:00 +02:00
if lxmessage . progress == None or lxmessage . progress < 0.01 : lxmessage . progress = 0.01
2024-09-07 11:35:17 +02:00
2022-06-17 08:54:04 +02:00
# Outbound handling for opportunistic messages
2023-11-02 18:29:46 +01:00
if lxmessage . method == LXMessage . OPPORTUNISTIC :
2022-06-17 08:54:04 +02:00
if lxmessage . delivery_attempts < = LXMRouter . MAX_DELIVERY_ATTEMPTS :
2024-09-16 17:49:54 +02:00
if lxmessage . delivery_attempts > = LXMRouter . MAX_PATHLESS_TRIES and not RNS . Transport . has_path ( lxmessage . get_destination ( ) . hash ) :
RNS . log ( f " Requesting path to { RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) } after { lxmessage . delivery_attempts } pathless tries for { lxmessage } " , RNS . LOG_DEBUG )
2022-06-17 08:54:04 +02:00
lxmessage . delivery_attempts + = 1
2024-09-16 17:49:54 +02:00
RNS . Transport . request_path ( lxmessage . get_destination ( ) . hash )
lxmessage . next_delivery_attempt = time . time ( ) + LXMRouter . PATH_REQUEST_WAIT
2024-10-11 23:40:27 +02:00
lxmessage . progress = 0.01
2024-09-19 23:00:24 +02:00
elif lxmessage . delivery_attempts == LXMRouter . MAX_PATHLESS_TRIES + 1 and RNS . Transport . has_path ( lxmessage . get_destination ( ) . hash ) :
RNS . log ( f " Opportunistic delivery for { lxmessage } still unsuccessful after { lxmessage . delivery_attempts } attempts, trying to rediscover path to { RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) } " , RNS . LOG_DEBUG )
2024-09-16 17:49:54 +02:00
lxmessage . delivery_attempts + = 1
2024-09-19 23:00:24 +02:00
RNS . Reticulum . get_instance ( ) . drop_path ( lxmessage . get_destination ( ) . hash )
def rediscover_job ( ) :
time . sleep ( 0.5 )
RNS . Transport . request_path ( lxmessage . get_destination ( ) . hash )
threading . Thread ( target = rediscover_job , daemon = True ) . start ( )
2024-09-16 17:49:54 +02:00
lxmessage . next_delivery_attempt = time . time ( ) + LXMRouter . PATH_REQUEST_WAIT
2024-10-11 23:40:27 +02:00
lxmessage . progress = 0.01
2024-09-16 17:49:54 +02:00
else :
if not hasattr ( lxmessage , " next_delivery_attempt " ) or time . time ( ) > lxmessage . next_delivery_attempt :
lxmessage . delivery_attempts + = 1
lxmessage . next_delivery_attempt = time . time ( ) + LXMRouter . DELIVERY_RETRY_WAIT
RNS . log ( " Opportunistic delivery attempt " + str ( lxmessage . delivery_attempts ) + " for " + str ( lxmessage ) + " to " + RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) , RNS . LOG_DEBUG )
lxmessage . send ( )
2022-06-17 08:54:04 +02:00
else :
RNS . log ( " Max delivery attempts reached for oppertunistic " + str ( lxmessage ) + " to " + RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) , RNS . LOG_DEBUG )
self . fail_message ( lxmessage )
# Outbound handling for messages transferred
# over a direct link to the final recipient
elif lxmessage . method == LXMessage . DIRECT :
if lxmessage . delivery_attempts < = LXMRouter . MAX_DELIVERY_ATTEMPTS :
delivery_destination_hash = lxmessage . get_destination ( ) . hash
2024-09-12 17:57:26 +02:00
direct_link = None
2022-06-17 08:54:04 +02:00
if delivery_destination_hash in self . direct_links :
2024-09-12 17:57:26 +02:00
# An established direct link already exists to
# the destination, so we'll try to use it for
# delivering the message
2022-06-17 08:54:04 +02:00
direct_link = self . direct_links [ delivery_destination_hash ]
2024-09-12 17:57:26 +02:00
RNS . log ( f " Using available direct link { direct_link } to { RNS . prettyhexrep ( delivery_destination_hash ) } " , RNS . LOG_DEBUG )
elif delivery_destination_hash in self . backchannel_links :
# An established backchannel link exists to
# the destination, so we'll try to use it for
# delivering the message
direct_link = self . backchannel_links [ delivery_destination_hash ]
RNS . log ( f " Using available backchannel link { direct_link } to { RNS . prettyhexrep ( delivery_destination_hash ) } " , RNS . LOG_DEBUG )
if direct_link != None :
2022-06-17 08:54:04 +02:00
if direct_link . status == RNS . Link . ACTIVE :
2024-03-19 11:03:12 +01:00
if lxmessage . progress == None or lxmessage . progress < 0.05 :
lxmessage . progress = 0.05
2022-06-17 08:54:04 +02:00
if lxmessage . state != LXMessage . SENDING :
2024-09-12 17:57:26 +02:00
RNS . log ( " Starting transfer of " + str ( lxmessage ) + " to " + RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) + " on link " + str ( direct_link ) , RNS . LOG_DEBUG )
2022-06-17 08:54:04 +02:00
lxmessage . set_delivery_destination ( direct_link )
lxmessage . send ( )
else :
if lxmessage . representation == LXMessage . RESOURCE :
RNS . log ( " The transfer of " + str ( lxmessage ) + " is in progress ( " + str ( round ( lxmessage . progress * 100 , 1 ) ) + " % ) " , RNS . LOG_DEBUG )
else :
RNS . log ( " Waiting for proof for " + str ( lxmessage ) + " sent as link packet " , RNS . LOG_DEBUG )
elif direct_link . status == RNS . Link . CLOSED :
if direct_link . activated_at != None :
2025-07-13 13:24:46 +02:00
RNS . log ( " The link to " + RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) + " was closed unexpectedly, retrying path request... " , RNS . LOG_DEBUG )
RNS . Transport . request_path ( lxmessage . get_destination ( ) . hash )
2022-06-17 08:54:04 +02:00
else :
if not hasattr ( lxmessage , " path_request_retried " ) :
RNS . log ( " The link to " + RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) + " was never activated, retrying path request... " , RNS . LOG_DEBUG )
RNS . Transport . request_path ( lxmessage . get_destination ( ) . hash )
lxmessage . path_request_retried = True
else :
RNS . log ( " The link to " + RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) + " was never activated " , RNS . LOG_DEBUG )
lxmessage . next_delivery_attempt = time . time ( ) + LXMRouter . PATH_REQUEST_WAIT
lxmessage . set_delivery_destination ( None )
2024-09-12 17:57:26 +02:00
if delivery_destination_hash in self . direct_links :
self . direct_links . pop ( delivery_destination_hash )
if delivery_destination_hash in self . backchannel_links :
self . backchannel_links . pop ( delivery_destination_hash )
2022-06-17 08:54:04 +02:00
lxmessage . next_delivery_attempt = time . time ( ) + LXMRouter . DELIVERY_RETRY_WAIT
else :
2024-03-19 11:03:12 +01:00
# Simply wait for the link to become active or close
2022-06-17 08:54:04 +02:00
RNS . log ( " The link to " + RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) + " is pending, waiting for link to become active " , RNS . LOG_DEBUG )
else :
# No link exists, so we'll try to establish one, but
# only if we've never tried before, or the retry wait
# period has elapsed.
if not hasattr ( lxmessage , " next_delivery_attempt " ) or time . time ( ) > lxmessage . next_delivery_attempt :
lxmessage . delivery_attempts + = 1
lxmessage . next_delivery_attempt = time . time ( ) + LXMRouter . DELIVERY_RETRY_WAIT
if lxmessage . delivery_attempts < LXMRouter . MAX_DELIVERY_ATTEMPTS :
if RNS . Transport . has_path ( lxmessage . get_destination ( ) . hash ) :
RNS . log ( " Establishing link to " + RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) + " for delivery attempt " + str ( lxmessage . delivery_attempts ) + " to " + RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) , RNS . LOG_DEBUG )
delivery_link = RNS . Link ( lxmessage . get_destination ( ) )
delivery_link . set_link_established_callback ( self . process_outbound )
self . direct_links [ delivery_destination_hash ] = delivery_link
2024-03-19 11:03:12 +01:00
lxmessage . progress = 0.03
2022-06-17 08:54:04 +02:00
else :
RNS . log ( " No path known for delivery attempt " + str ( lxmessage . delivery_attempts ) + " to " + RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) + " . Requesting path... " , RNS . LOG_DEBUG )
RNS . Transport . request_path ( lxmessage . get_destination ( ) . hash )
lxmessage . next_delivery_attempt = time . time ( ) + LXMRouter . PATH_REQUEST_WAIT
2024-10-11 23:40:27 +02:00
lxmessage . progress = 0.01
2022-06-17 08:54:04 +02:00
else :
RNS . log ( " Max delivery attempts reached for direct " + str ( lxmessage ) + " to " + RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) , RNS . LOG_DEBUG )
self . fail_message ( lxmessage )
# Outbound handling for messages transported via
# propagation to a LXMF router network.
elif lxmessage . method == LXMessage . PROPAGATED :
RNS . log ( " Attempting propagated delivery for " + str ( lxmessage ) + " to " + RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) , RNS . LOG_DEBUG )
if self . outbound_propagation_node == None :
RNS . log ( " No outbound propagation node specified for propagated " + str ( lxmessage ) + " to " + RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) , RNS . LOG_ERROR )
self . fail_message ( lxmessage )
else :
if lxmessage . delivery_attempts < = LXMRouter . MAX_DELIVERY_ATTEMPTS :
if self . outbound_propagation_link != None :
# A link already exists, so we'll try to use it
# to deliver the message
if self . outbound_propagation_link . status == RNS . Link . ACTIVE :
if lxmessage . state != LXMessage . SENDING :
RNS . log ( " Starting propagation transfer of " + str ( lxmessage ) + " to " + RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) + " via " + RNS . prettyhexrep ( self . outbound_propagation_node ) , RNS . LOG_DEBUG )
lxmessage . set_delivery_destination ( self . outbound_propagation_link )
lxmessage . send ( )
else :
if lxmessage . representation == LXMessage . RESOURCE :
RNS . log ( " The transfer of " + str ( lxmessage ) + " is in progress ( " + str ( round ( lxmessage . progress * 100 , 1 ) ) + " % ) " , RNS . LOG_DEBUG )
else :
RNS . log ( " Waiting for proof for " + str ( lxmessage ) + " sent as link packet " , RNS . LOG_DEBUG )
elif self . outbound_propagation_link . status == RNS . Link . CLOSED :
RNS . log ( " The link to " + RNS . prettyhexrep ( self . outbound_propagation_node ) + " was closed " , RNS . LOG_DEBUG )
self . outbound_propagation_link = None
lxmessage . next_delivery_attempt = time . time ( ) + LXMRouter . DELIVERY_RETRY_WAIT
else :
# Simply wait for the link to become
# active or close
2024-02-29 23:02:16 +01:00
RNS . log ( " The propagation link to " + RNS . prettyhexrep ( self . outbound_propagation_node ) + " is pending, waiting for link to become active " , RNS . LOG_DEBUG )
2022-06-17 08:54:04 +02:00
else :
# No link exists, so we'll try to establish one, but
# only if we've never tried before, or the retry wait
# period has elapsed.
if not hasattr ( lxmessage , " next_delivery_attempt " ) or time . time ( ) > lxmessage . next_delivery_attempt :
lxmessage . delivery_attempts + = 1
lxmessage . next_delivery_attempt = time . time ( ) + LXMRouter . DELIVERY_RETRY_WAIT
if lxmessage . delivery_attempts < LXMRouter . MAX_DELIVERY_ATTEMPTS :
if RNS . Transport . has_path ( self . outbound_propagation_node ) :
RNS . log ( " Establishing link to " + RNS . prettyhexrep ( self . outbound_propagation_node ) + " for propagation attempt " + str ( lxmessage . delivery_attempts ) + " to " + RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) , RNS . LOG_DEBUG )
propagation_node_identity = RNS . Identity . recall ( self . outbound_propagation_node )
propagation_node_destination = RNS . Destination ( propagation_node_identity , RNS . Destination . OUT , RNS . Destination . SINGLE , APP_NAME , " propagation " )
self . outbound_propagation_link = RNS . Link ( propagation_node_destination , established_callback = self . process_outbound )
2025-10-31 21:45:40 +01:00
self . outbound_propagation_link . set_packet_callback ( self . propagation_transfer_signalling_packet )
self . outbound_propagation_link . for_lxmessage = lxmessage
2022-06-17 08:54:04 +02:00
else :
RNS . log ( " No path known for propagation attempt " + str ( lxmessage . delivery_attempts ) + " to " + RNS . prettyhexrep ( self . outbound_propagation_node ) + " . Requesting path... " , RNS . LOG_DEBUG )
RNS . Transport . request_path ( self . outbound_propagation_node )
lxmessage . next_delivery_attempt = time . time ( ) + LXMRouter . PATH_REQUEST_WAIT
else :
RNS . log ( " Max delivery attempts reached for propagated " + str ( lxmessage ) + " to " + RNS . prettyhexrep ( lxmessage . get_destination ( ) . hash ) , RNS . LOG_DEBUG )
self . fail_message ( lxmessage )