Refactored transport jobs for free-threaded implementation

This commit is contained in:
Mark Qvist
2026-04-12 13:33:15 +02:00
parent 9f817bd918
commit fa353fb0b3
+332 -319
View File
@@ -124,8 +124,14 @@ class Transport:
path_table_lock = Lock() path_table_lock = Lock()
reverse_table_lock = Lock() reverse_table_lock = Lock()
link_table_lock = Lock() link_table_lock = Lock()
active_links_lock = Lock()
pending_links_lock = Lock()
tunnels_lock = Lock() tunnels_lock = Lock()
receipts_lock = Lock()
discovery_pr_lock = Lock() discovery_pr_lock = Lock()
discovery_pr_tags_lock = Lock()
path_requests_lock = Lock()
pending_local_prs_lock = Lock()
path_states_lock = Lock() path_states_lock = Lock()
jobs_lock = Lock() jobs_lock = Lock()
@@ -470,118 +476,128 @@ class Transport:
Transport.jobs_running = True Transport.jobs_running = True
try: try:
if not Transport.jobs_locked: with Transport.jobs_lock:
should_collect = False should_collect = False
# Process active and pending link lists # Process active and pending link lists
if time.time() > Transport.links_last_checked+Transport.links_check_interval: if time.time() > Transport.links_last_checked+Transport.links_check_interval:
for link in Transport.pending_links: with Transport.pending_links_lock:
if link.status == RNS.Link.CLOSED: for link in Transport.pending_links:
# If we are not a Transport Instance, finding a pending link if link.status == RNS.Link.CLOSED:
# that was never activated will trigger an expiry of the path # If we are not a Transport Instance, finding a pending link
# to the destination, and an attempt to rediscover the path. # that was never activated will trigger an expiry of the path
if not RNS.Reticulum.transport_enabled(): # to the destination, and an attempt to rediscover the path.
Transport.expire_path(link.destination.hash) if not RNS.Reticulum.transport_enabled():
Transport.expire_path(link.destination.hash)
# If we are connected to a shared instance, it will take # If we are connected to a shared instance, it will take
# care of sending out a new path request. If not, we will # care of sending out a new path request. If not, we will
# send one directly. # send one directly.
if not Transport.owner.is_connected_to_shared_instance: if not Transport.owner.is_connected_to_shared_instance:
last_path_request = 0 last_path_request = 0
if link.destination.hash in Transport.path_requests: with Transport.path_requests_lock:
last_path_request = Transport.path_requests[link.destination.hash] if link.destination.hash in Transport.path_requests:
last_path_request = Transport.path_requests[link.destination.hash]
if time.time() - last_path_request > Transport.PATH_REQUEST_MI: if time.time() - last_path_request > Transport.PATH_REQUEST_MI:
RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link.destination.hash)+" since an attempted link was never established", RNS.LOG_DEBUG) RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link.destination.hash)+" since an attempted link was never established", RNS.LOG_DEBUG)
if not link.destination.hash in path_requests: if not link.destination.hash in path_requests:
blocked_if = None blocked_if = None
path_requests[link.destination.hash] = blocked_if path_requests[link.destination.hash] = blocked_if
Transport.pending_links.remove(link) Transport.pending_links.remove(link)
for link in Transport.active_links: with Transport.active_links_lock:
if link.status == RNS.Link.CLOSED: closed_links = []
Transport.active_links.remove(link) for link in Transport.active_links:
if link.status == RNS.Link.CLOSED: closed_links.append(link)
for closed_link in closed_links: Transport.active_links.remove(closed_link)
Transport.links_last_checked = time.time() Transport.links_last_checked = time.time()
# Process receipts list for timed-out packets # Process receipts list for timed-out packets
if time.time() > Transport.receipts_last_checked+Transport.receipts_check_interval: if time.time() > Transport.receipts_last_checked+Transport.receipts_check_interval:
while len(Transport.receipts) > Transport.MAX_RECEIPTS: with Transport.receipts_lock:
culled_receipt = Transport.receipts.pop(0) while len(Transport.receipts) > Transport.MAX_RECEIPTS:
culled_receipt.timeout = -1 culled_receipt = Transport.receipts.pop(0)
culled_receipt.check_timeout() culled_receipt.timeout = -1
should_collect = True culled_receipt.check_timeout()
should_collect = True
for receipt in Transport.receipts: with Transport.receipts_lock:
receipt.check_timeout() expired_receipts = []
if receipt.status != RNS.PacketReceipt.SENT: for receipt in Transport.receipts:
if receipt in Transport.receipts: receipt.check_timeout()
Transport.receipts.remove(receipt) if receipt.status != RNS.PacketReceipt.SENT: expired_receipts.append(receipt)
for expired_receipt in expired_receipts:
if expired_receipt in Transport.receipts: Transport.receipts.remove(expired_receipt)
Transport.receipts_last_checked = time.time() Transport.receipts_last_checked = time.time()
# Process announces needing retransmission # Process announces needing retransmission
if time.time() > Transport.announces_last_checked+Transport.announces_check_interval: if time.time() > Transport.announces_last_checked+Transport.announces_check_interval:
completed_announces = [] completed_announces = []
for destination_hash in Transport.announce_table:
announce_entry = Transport.announce_table[destination_hash] with Transport.announce_table_lock:
if announce_entry[IDX_AT_RETRIES] > 0 and announce_entry[IDX_AT_RETRIES] >= Transport.LOCAL_REBROADCASTS_MAX: for destination_hash in Transport.announce_table:
RNS.log("Completed announce processing for "+RNS.prettyhexrep(destination_hash)+", local rebroadcast limit reached", RNS.LOG_EXTREME) announce_entry = Transport.announce_table[destination_hash]
completed_announces.append(destination_hash) if announce_entry[IDX_AT_RETRIES] > 0 and announce_entry[IDX_AT_RETRIES] >= Transport.LOCAL_REBROADCASTS_MAX:
elif announce_entry[IDX_AT_RETRIES] > Transport.PATHFINDER_R: RNS.log("Completed announce processing for "+RNS.prettyhexrep(destination_hash)+", local rebroadcast limit reached", RNS.LOG_EXTREME)
RNS.log("Completed announce processing for "+RNS.prettyhexrep(destination_hash)+", retry limit reached", RNS.LOG_EXTREME) completed_announces.append(destination_hash)
completed_announces.append(destination_hash) elif announce_entry[IDX_AT_RETRIES] > Transport.PATHFINDER_R:
else: RNS.log("Completed announce processing for "+RNS.prettyhexrep(destination_hash)+", retry limit reached", RNS.LOG_EXTREME)
if time.time() > announce_entry[IDX_AT_RTRNS_TMO]: completed_announces.append(destination_hash)
announce_entry[IDX_AT_RTRNS_TMO] = time.time() + Transport.PATHFINDER_G + Transport.PATHFINDER_RW else:
announce_entry[IDX_AT_RETRIES] += 1 if time.time() > announce_entry[IDX_AT_RTRNS_TMO]:
packet = announce_entry[IDX_AT_PACKET] announce_entry[IDX_AT_RTRNS_TMO] = time.time() + Transport.PATHFINDER_G + Transport.PATHFINDER_RW
block_rebroadcasts = announce_entry[IDX_AT_BLCK_RBRD] announce_entry[IDX_AT_RETRIES] += 1
attached_interface = announce_entry[IDX_AT_ATTCHD_IF] packet = announce_entry[IDX_AT_PACKET]
announce_context = RNS.Packet.NONE block_rebroadcasts = announce_entry[IDX_AT_BLCK_RBRD]
if block_rebroadcasts: announce_context = RNS.Packet.PATH_RESPONSE attached_interface = announce_entry[IDX_AT_ATTCHD_IF]
announce_data = packet.data announce_context = RNS.Packet.NONE
announce_identity = RNS.Identity.recall(packet.destination_hash) if block_rebroadcasts: announce_context = RNS.Packet.PATH_RESPONSE
announce_destination = RNS.Destination(announce_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "unknown", "unknown"); announce_data = packet.data
announce_destination.hash = packet.destination_hash announce_identity = RNS.Identity.recall(packet.destination_hash)
announce_destination.hexhash = announce_destination.hash.hex() announce_destination = RNS.Destination(announce_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "unknown", "unknown");
announce_destination.hash = packet.destination_hash
new_packet = RNS.Packet( announce_destination.hexhash = announce_destination.hash.hex()
announce_destination,
announce_data, new_packet = RNS.Packet(
RNS.Packet.ANNOUNCE, announce_destination,
context = announce_context, announce_data,
header_type = RNS.Packet.HEADER_2, RNS.Packet.ANNOUNCE,
transport_type = Transport.TRANSPORT, context = announce_context,
transport_id = Transport.identity.hash, header_type = RNS.Packet.HEADER_2,
attached_interface = attached_interface, transport_type = Transport.TRANSPORT,
context_flag = packet.context_flag, transport_id = Transport.identity.hash,
) attached_interface = attached_interface,
context_flag = packet.context_flag,
)
new_packet.hops = announce_entry[4] new_packet.hops = announce_entry[4]
if block_rebroadcasts: if block_rebroadcasts:
RNS.log("Rebroadcasting announce as path response for "+RNS.prettyhexrep(announce_destination.hash)+" with hop count "+str(new_packet.hops), RNS.LOG_DEBUG) RNS.log("Rebroadcasting announce as path response for "+RNS.prettyhexrep(announce_destination.hash)+" with hop count "+str(new_packet.hops), RNS.LOG_DEBUG)
else: else:
RNS.log("Rebroadcasting announce for "+RNS.prettyhexrep(announce_destination.hash)+" with hop count "+str(new_packet.hops), RNS.LOG_DEBUG) RNS.log("Rebroadcasting announce for "+RNS.prettyhexrep(announce_destination.hash)+" with hop count "+str(new_packet.hops), RNS.LOG_DEBUG)
outgoing.append(new_packet) outgoing.append(new_packet)
# This handles an edge case where a peer sends a past # This handles an edge case where a peer sends a past
# request for a destination just after an announce for # request for a destination just after an announce for
# said destination has arrived, but before it has been # said destination has arrived, but before it has been
# rebroadcast locally. In such a case the actual announce # rebroadcast locally. In such a case the actual announce
# is temporarily held, and then reinserted when the path # is temporarily held, and then reinserted when the path
# request has been served to the peer. # request has been served to the peer.
if destination_hash in Transport.held_announces: if destination_hash in Transport.held_announces:
held_entry = Transport.held_announces.pop(destination_hash) held_entry = Transport.held_announces.pop(destination_hash)
Transport.announce_table[destination_hash] = held_entry Transport.announce_table[destination_hash] = held_entry
RNS.log("Reinserting held announce into table", RNS.LOG_DEBUG) RNS.log("Reinserting held announce into table", RNS.LOG_DEBUG)
for destination_hash in completed_announces: for destination_hash in completed_announces:
if destination_hash in Transport.announce_table: if destination_hash in Transport.announce_table: Transport.announce_table.pop(destination_hash)
Transport.announce_table.pop(destination_hash)
Transport.announces_last_checked = time.time() Transport.announces_last_checked = time.time()
@@ -593,233 +609,244 @@ class Transport:
# Cull invalidated path requests # Cull invalidated path requests
if time.time() > Transport.pending_prs_last_checked+Transport.pending_prs_check_interval: if time.time() > Transport.pending_prs_last_checked+Transport.pending_prs_check_interval:
for destination_hash in Transport.pending_local_path_requests.copy(): with Transport.pending_local_prs_lock:
if not Transport.pending_local_path_requests[destination_hash] in Transport.interfaces: for destination_hash in Transport.pending_local_path_requests:
Transport.pending_local_path_requests.pop(destination_hash) if not Transport.pending_local_path_requests[destination_hash] in Transport.interfaces:
Transport.pending_local_path_requests.pop(destination_hash)
Transport.pending_prs_last_checked = time.time() Transport.pending_prs_last_checked = time.time()
# Cull the path request tags list if it has reached its max size # Cull the path request tags list if it has reached its max size
if len(Transport.discovery_pr_tags) > Transport.max_pr_tags: if len(Transport.discovery_pr_tags) > Transport.max_pr_tags:
Transport.discovery_pr_tags = Transport.discovery_pr_tags[len(Transport.discovery_pr_tags)-Transport.max_pr_tags:len(Transport.discovery_pr_tags)-1] with Transport.discovery_pr_tags_lock:
Transport.discovery_pr_tags = Transport.discovery_pr_tags[len(Transport.discovery_pr_tags)-Transport.max_pr_tags:len(Transport.discovery_pr_tags)-1]
if time.time() > Transport.tables_last_culled + Transport.tables_cull_interval: if time.time() > Transport.tables_last_culled + Transport.tables_cull_interval:
# Remove unneeded path state entries # Remove unneeded path state entries
stale_path_states = [] stale_path_states = []
for destination_hash in Transport.path_states: with Transport.path_states_lock:
if not destination_hash in Transport.path_table: for destination_hash in Transport.path_states:
stale_path_states.append(destination_hash) if not destination_hash in Transport.path_table:
stale_path_states.append(destination_hash)
# Cull the reverse table according to timeout # Cull the reverse table according to timeout
stale_reverse_entries = [] stale_reverse_entries = []
for truncated_packet_hash in Transport.reverse_table: with Transport.reverse_table_lock:
reverse_entry = Transport.reverse_table[truncated_packet_hash] for truncated_packet_hash in Transport.reverse_table:
if time.time() > reverse_entry[IDX_RT_TIMESTAMP] + Transport.REVERSE_TIMEOUT: reverse_entry = Transport.reverse_table[truncated_packet_hash]
stale_reverse_entries.append(truncated_packet_hash) if time.time() > reverse_entry[IDX_RT_TIMESTAMP] + Transport.REVERSE_TIMEOUT: stale_reverse_entries.append(truncated_packet_hash)
elif not reverse_entry[IDX_RT_OUTB_IF] in Transport.interfaces: elif not reverse_entry[IDX_RT_OUTB_IF] in Transport.interfaces: stale_reverse_entries.append(truncated_packet_hash)
stale_reverse_entries.append(truncated_packet_hash) elif not reverse_entry[IDX_RT_RCVD_IF] in Transport.interfaces: stale_reverse_entries.append(truncated_packet_hash)
elif not reverse_entry[IDX_RT_RCVD_IF] in Transport.interfaces:
stale_reverse_entries.append(truncated_packet_hash)
# Cull the link table according to timeout # Cull the link table according to timeout
stale_links = [] stale_links = []
for link_id in Transport.link_table: with Transport.link_table_lock:
link_entry = Transport.link_table[link_id] for link_id in Transport.link_table:
link_entry = Transport.link_table[link_id]
if link_entry[IDX_LT_VALIDATED] == True: if link_entry[IDX_LT_VALIDATED] == True:
if time.time() > link_entry[IDX_LT_TIMESTAMP] + Transport.LINK_TIMEOUT: if time.time() > link_entry[IDX_LT_TIMESTAMP] + Transport.LINK_TIMEOUT: stale_links.append(link_id)
stale_links.append(link_id) elif not link_entry[IDX_LT_NH_IF] in Transport.interfaces: stale_links.append(link_id)
elif not link_entry[IDX_LT_NH_IF] in Transport.interfaces: elif not link_entry[IDX_LT_RCVD_IF] in Transport.interfaces: stale_links.append(link_id)
stale_links.append(link_id)
elif not link_entry[IDX_LT_RCVD_IF] in Transport.interfaces: else:
stale_links.append(link_id) if time.time() > link_entry[IDX_LT_PROOF_TMO]:
else: stale_links.append(link_id)
if time.time() > link_entry[IDX_LT_PROOF_TMO]:
stale_links.append(link_id)
last_path_request = 0 last_path_request = 0
if link_entry[IDX_LT_DSTHASH] in Transport.path_requests: with Transport.path_requests_lock:
last_path_request = Transport.path_requests[link_entry[IDX_LT_DSTHASH]] if link_entry[IDX_LT_DSTHASH] in Transport.path_requests:
last_path_request = Transport.path_requests[link_entry[IDX_LT_DSTHASH]]
lr_taken_hops = link_entry[IDX_LT_HOPS] lr_taken_hops = link_entry[IDX_LT_HOPS]
path_request_throttle = time.time() - last_path_request < Transport.PATH_REQUEST_MI path_request_throttle = time.time() - last_path_request < Transport.PATH_REQUEST_MI
path_request_conditions = False path_request_conditions = False
# If the path has been invalidated between the time of # If the path has been invalidated between the time of
# making the link request and now, try to rediscover it # making the link request and now, try to rediscover it
if not Transport.has_path(link_entry[IDX_LT_DSTHASH]): if not Transport.has_path(link_entry[IDX_LT_DSTHASH]):
RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link_entry[IDX_LT_DSTHASH])+" since an attempted link was never established, and path is now missing", RNS.LOG_DEBUG) RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link_entry[IDX_LT_DSTHASH])+" since an attempted link was never established, and path is now missing", RNS.LOG_DEBUG)
path_request_conditions =True path_request_conditions = True
# If this link request was originated from a local client # If this link request was originated from a local client
# attempt to rediscover a path to the destination, if this # attempt to rediscover a path to the destination, if this
# has not already happened recently. # has not already happened recently.
elif not path_request_throttle and lr_taken_hops == 0: elif not path_request_throttle and lr_taken_hops == 0:
RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link_entry[IDX_LT_DSTHASH])+" since an attempted local client link was never established", RNS.LOG_DEBUG) RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link_entry[IDX_LT_DSTHASH])+" since an attempted local client link was never established", RNS.LOG_DEBUG)
path_request_conditions = True path_request_conditions = True
# If the link destination was previously only 1 hop # If the link destination was previously only 1 hop
# away, this likely means that it was local to one # away, this likely means that it was local to one
# of our interfaces, and that it roamed somewhere else. # of our interfaces, and that it roamed somewhere else.
# In that case, try to discover a new path, and mark # In that case, try to discover a new path, and mark
# the old one as unresponsive. # the old one as unresponsive.
elif not path_request_throttle and Transport.hops_to(link_entry[IDX_LT_DSTHASH]) == 1: elif not path_request_throttle and Transport.hops_to(link_entry[IDX_LT_DSTHASH]) == 1:
RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link_entry[IDX_LT_DSTHASH])+" since an attempted link was never established, and destination was previously local to an interface on this instance", RNS.LOG_DEBUG) RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link_entry[IDX_LT_DSTHASH])+" since an attempted link was never established, and destination was previously local to an interface on this instance", RNS.LOG_DEBUG)
path_request_conditions = True path_request_conditions = True
blocked_if = link_entry[IDX_LT_RCVD_IF] blocked_if = link_entry[IDX_LT_RCVD_IF]
# TODO: This might result in the path re-resolution # TODO: This might result in the path re-resolution
# only being able to happen once, since new path found # only being able to happen once, since new path found
# after allowing update from higher hop-count path, after # after allowing update from higher hop-count path, after
# marking old path unresponsive, might be more than 1 hop away, # marking old path unresponsive, might be more than 1 hop away,
# thus dealocking us into waiting for a new announce all-together. # thus dealocking us into waiting for a new announce all-together.
# Is this problematic, or does it actually not matter? # Is this problematic, or does it actually not matter?
# Best would be to have full support for alternative paths, # Best would be to have full support for alternative paths,
# and score them according to number of unsuccessful tries or # and score them according to number of unsuccessful tries or
# similar. # similar.
if RNS.Reticulum.transport_enabled(): if RNS.Reticulum.transport_enabled():
if hasattr(link_entry[IDX_LT_RCVD_IF], "mode") and link_entry[IDX_LT_RCVD_IF].mode != RNS.Interfaces.Interface.Interface.MODE_BOUNDARY: if hasattr(link_entry[IDX_LT_RCVD_IF], "mode") and link_entry[IDX_LT_RCVD_IF].mode != RNS.Interfaces.Interface.Interface.MODE_BOUNDARY:
Transport.mark_path_unresponsive(link_entry[IDX_LT_DSTHASH]) Transport.mark_path_unresponsive(link_entry[IDX_LT_DSTHASH])
# If the link initiator is only 1 hop away, # If the link initiator is only 1 hop away,
# this likely means that network topology has # this likely means that network topology has
# changed. In that case, we try to discover a new path, # changed. In that case, we try to discover a new path,
# and mark the old one as potentially unresponsive. # and mark the old one as potentially unresponsive.
elif not path_request_throttle and lr_taken_hops == 1: elif not path_request_throttle and lr_taken_hops == 1:
RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link_entry[IDX_LT_DSTHASH])+" since an attempted link was never established, and link initiator is local to an interface on this instance", RNS.LOG_DEBUG) RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link_entry[IDX_LT_DSTHASH])+" since an attempted link was never established, and link initiator is local to an interface on this instance", RNS.LOG_DEBUG)
path_request_conditions = True path_request_conditions = True
blocked_if = link_entry[IDX_LT_RCVD_IF] blocked_if = link_entry[IDX_LT_RCVD_IF]
if RNS.Reticulum.transport_enabled(): if RNS.Reticulum.transport_enabled():
if hasattr(link_entry[IDX_LT_RCVD_IF], "mode") and link_entry[IDX_LT_RCVD_IF].mode != RNS.Interfaces.Interface.Interface.MODE_BOUNDARY: if hasattr(link_entry[IDX_LT_RCVD_IF], "mode") and link_entry[IDX_LT_RCVD_IF].mode != RNS.Interfaces.Interface.Interface.MODE_BOUNDARY:
Transport.mark_path_unresponsive(link_entry[IDX_LT_DSTHASH]) Transport.mark_path_unresponsive(link_entry[IDX_LT_DSTHASH])
if path_request_conditions: if path_request_conditions:
if not link_entry[IDX_LT_DSTHASH] in path_requests: with Transport.path_requests_lock:
path_requests[link_entry[IDX_LT_DSTHASH]] = blocked_if if not link_entry[IDX_LT_DSTHASH] in path_requests:
path_requests[link_entry[IDX_LT_DSTHASH]] = blocked_if
if not RNS.Reticulum.transport_enabled(): if not RNS.Reticulum.transport_enabled():
# Drop current path if we are not a transport instance, to # Drop current path if we are not a transport instance, to
# allow using higher-hop count paths or reused announces # allow using higher-hop count paths or reused announces
# from newly adjacent transport instances. # from newly adjacent transport instances.
Transport.expire_path(link_entry[IDX_LT_DSTHASH]) Transport.expire_path(link_entry[IDX_LT_DSTHASH])
# Cull the path table # Cull the path table
stale_paths = [] stale_paths = []
for destination_hash in Transport.path_table: with Transport.path_table_lock:
destination_entry = Transport.path_table[destination_hash] for destination_hash in Transport.path_table:
attached_interface = destination_entry[IDX_PT_RVCD_IF] destination_entry = Transport.path_table[destination_hash]
attached_interface = destination_entry[IDX_PT_RVCD_IF]
if attached_interface != None and hasattr(attached_interface, "mode") and attached_interface.mode == RNS.Interfaces.Interface.Interface.MODE_ACCESS_POINT: if attached_interface != None and hasattr(attached_interface, "mode") and attached_interface.mode == RNS.Interfaces.Interface.Interface.MODE_ACCESS_POINT:
destination_expiry = destination_entry[IDX_PT_TIMESTAMP] + Transport.AP_PATH_TIME destination_expiry = destination_entry[IDX_PT_TIMESTAMP] + Transport.AP_PATH_TIME
elif attached_interface != None and hasattr(attached_interface, "mode") and attached_interface.mode == RNS.Interfaces.Interface.Interface.MODE_ROAMING: elif attached_interface != None and hasattr(attached_interface, "mode") and attached_interface.mode == RNS.Interfaces.Interface.Interface.MODE_ROAMING:
destination_expiry = destination_entry[IDX_PT_TIMESTAMP] + Transport.ROAMING_PATH_TIME destination_expiry = destination_entry[IDX_PT_TIMESTAMP] + Transport.ROAMING_PATH_TIME
else: else:
destination_expiry = destination_entry[IDX_PT_TIMESTAMP] + Transport.DESTINATION_TIMEOUT destination_expiry = destination_entry[IDX_PT_TIMESTAMP] + Transport.DESTINATION_TIMEOUT
if time.time() > destination_expiry: if time.time() > destination_expiry:
stale_paths.append(destination_hash) stale_paths.append(destination_hash)
should_collect = True should_collect = True
RNS.log("Path to "+RNS.prettyhexrep(destination_hash)+" timed out and was removed", RNS.LOG_DEBUG) RNS.log("Path to "+RNS.prettyhexrep(destination_hash)+" timed out and was removed", RNS.LOG_DEBUG)
elif not attached_interface in Transport.interfaces: elif not attached_interface in Transport.interfaces:
stale_paths.append(destination_hash) stale_paths.append(destination_hash)
should_collect = True should_collect = True
RNS.log("Path to "+RNS.prettyhexrep(destination_hash)+" was removed since the attached interface no longer exists", RNS.LOG_DEBUG) RNS.log("Path to "+RNS.prettyhexrep(destination_hash)+" was removed since the attached interface no longer exists", RNS.LOG_DEBUG)
# Cull the pending discovery path requests table # Cull the pending discovery path requests table
stale_discovery_path_requests = [] stale_discovery_path_requests = []
for destination_hash in Transport.discovery_path_requests: with Transport.discovery_pr_lock:
entry = Transport.discovery_path_requests[destination_hash] for destination_hash in Transport.discovery_path_requests:
entry = Transport.discovery_path_requests[destination_hash]
if time.time() > entry["timeout"]: if time.time() > entry["timeout"]:
stale_discovery_path_requests.append(destination_hash) stale_discovery_path_requests.append(destination_hash)
should_collect = True should_collect = True
RNS.log("Waiting path request for "+RNS.prettyhexrep(destination_hash)+" timed out and was removed", RNS.LOG_DEBUG) RNS.log("Waiting path request for "+RNS.prettyhexrep(destination_hash)+" timed out and was removed", RNS.LOG_DEBUG)
# Cull the tunnel table # Cull the tunnel table
stale_tunnels = []; ti = 0 stale_tunnels = []; ti = 0
for tunnel_id in Transport.tunnels: with Transport.tunnels_lock:
tunnel_entry = Transport.tunnels[tunnel_id] for tunnel_id in Transport.tunnels:
tunnel_entry = Transport.tunnels[tunnel_id]
expires = tunnel_entry[IDX_TT_EXPIRES] expires = tunnel_entry[IDX_TT_EXPIRES]
if time.time() > expires: if time.time() > expires:
stale_tunnels.append(tunnel_id) stale_tunnels.append(tunnel_id)
should_collect = True should_collect = True
RNS.log("Tunnel "+RNS.prettyhexrep(tunnel_id)+" timed out and was removed", RNS.LOG_EXTREME) RNS.log("Tunnel "+RNS.prettyhexrep(tunnel_id)+" timed out and was removed", RNS.LOG_EXTREME)
else:
if tunnel_entry[IDX_TT_IF] and not tunnel_entry[IDX_TT_IF] in Transport.interfaces: else:
RNS.log(f"Removing non-existent tunnel interface {tunnel_entry[IDX_TT_IF]}", RNS.LOG_EXTREME) if tunnel_entry[IDX_TT_IF] and not tunnel_entry[IDX_TT_IF] in Transport.interfaces:
tunnel_entry[IDX_TT_IF] = None RNS.log(f"Removing non-existent tunnel interface {tunnel_entry[IDX_TT_IF]}", RNS.LOG_EXTREME)
tunnel_entry[IDX_TT_IF] = None
stale_tunnel_paths = [] stale_tunnel_paths = []
tunnel_paths = tunnel_entry[IDX_TT_PATHS] tunnel_paths = tunnel_entry[IDX_TT_PATHS]
for tunnel_path in tunnel_paths: for tunnel_path in tunnel_paths:
tunnel_path_entry = tunnel_paths[tunnel_path] tunnel_path_entry = tunnel_paths[tunnel_path]
if time.time() > tunnel_path_entry[0] + Transport.DESTINATION_TIMEOUT: if time.time() > tunnel_path_entry[0] + Transport.DESTINATION_TIMEOUT:
stale_tunnel_paths.append(tunnel_path) stale_tunnel_paths.append(tunnel_path)
should_collect = True should_collect = True
RNS.log("Tunnel path to "+RNS.prettyhexrep(tunnel_path)+" timed out and was removed", RNS.LOG_EXTREME) RNS.log("Tunnel path to "+RNS.prettyhexrep(tunnel_path)+" timed out and was removed", RNS.LOG_EXTREME)
for tunnel_path in stale_tunnel_paths:
tunnel_paths.pop(tunnel_path)
ti += 1
for tunnel_path in stale_tunnel_paths:
tunnel_paths.pop(tunnel_path)
ti += 1
if ti > 0: if ti > 0:
if ti == 1: RNS.log("Removed "+str(ti)+" tunnel path", RNS.LOG_EXTREME) if ti == 1: RNS.log("Removed "+str(ti)+" tunnel path", RNS.LOG_EXTREME)
else: RNS.log("Removed "+str(ti)+" tunnel paths", RNS.LOG_EXTREME) else: RNS.log("Removed "+str(ti)+" tunnel paths", RNS.LOG_EXTREME)
i = 0 i = 0
for truncated_packet_hash in stale_reverse_entries: with Transport.reverse_table_lock:
Transport.reverse_table.pop(truncated_packet_hash) for truncated_packet_hash in stale_reverse_entries:
i += 1 if truncated_packet_hash in Transport.reverse_table: Transport.reverse_table.pop(truncated_packet_hash)
i += 1
if i > 0: if i > 0:
if i == 1: RNS.log("Released "+str(i)+" reverse table entry", RNS.LOG_EXTREME) if i == 1: RNS.log("Released "+str(i)+" reverse table entry", RNS.LOG_EXTREME)
else: RNS.log("Released "+str(i)+" reverse table entries", RNS.LOG_EXTREME) else: RNS.log("Released "+str(i)+" reverse table entries", RNS.LOG_EXTREME)
i = 0 i = 0
for link_id in stale_links: with Transport.link_table_lock:
Transport.link_table.pop(link_id) for link_id in stale_links:
i += 1 Transport.link_table.pop(link_id)
i += 1
if i > 0: if i > 0:
if i == 1: RNS.log("Released "+str(i)+" link", RNS.LOG_EXTREME) if i == 1: RNS.log("Released "+str(i)+" link", RNS.LOG_EXTREME)
else: RNS.log("Released "+str(i)+" links", RNS.LOG_EXTREME) else: RNS.log("Released "+str(i)+" links", RNS.LOG_EXTREME)
i = 0 i = 0
for destination_hash in stale_paths: with Transport.path_table_lock:
Transport.path_table.pop(destination_hash) for destination_hash in stale_paths:
i += 1 if destination_hash in Transport.path_table: Transport.path_table.pop(destination_hash)
i += 1
if i > 0: if i > 0:
if i == 1: RNS.log("Removed "+str(i)+" path", RNS.LOG_EXTREME) if i == 1: RNS.log("Removed "+str(i)+" path", RNS.LOG_EXTREME)
else: RNS.log("Removed "+str(i)+" paths", RNS.LOG_EXTREME) else: RNS.log("Removed "+str(i)+" paths", RNS.LOG_EXTREME)
i = 0 i = 0
for destination_hash in stale_discovery_path_requests: with Transport.discovery_pr_lock:
Transport.discovery_path_requests.pop(destination_hash) for destination_hash in stale_discovery_path_requests:
i += 1 Transport.discovery_path_requests.pop(destination_hash)
i += 1
if i > 0: if i > 0:
if i == 1: RNS.log("Removed "+str(i)+" waiting path request", RNS.LOG_EXTREME) if i == 1: RNS.log("Removed "+str(i)+" waiting path request", RNS.LOG_EXTREME)
else: RNS.log("Removed "+str(i)+" waiting path requests", RNS.LOG_EXTREME) else: RNS.log("Removed "+str(i)+" waiting path requests", RNS.LOG_EXTREME)
i = 0 i = 0
for tunnel_id in stale_tunnels: with Transport.tunnels_lock:
Transport.tunnels.pop(tunnel_id) for tunnel_id in stale_tunnels:
i += 1 Transport.tunnels.pop(tunnel_id)
i += 1
if i > 0: if i > 0:
if i == 1: RNS.log("Removed "+str(i)+" tunnel", RNS.LOG_EXTREME) if i == 1: RNS.log("Removed "+str(i)+" tunnel", RNS.LOG_EXTREME)
else: RNS.log("Removed "+str(i)+" tunnels", RNS.LOG_EXTREME) else: RNS.log("Removed "+str(i)+" tunnels", RNS.LOG_EXTREME)
i = 0 i = 0
for destination_hash in stale_path_states: with Transport.path_states_lock:
Transport.path_states.pop(destination_hash) for destination_hash in stale_path_states:
i += 1 Transport.path_states.pop(destination_hash)
i += 1
if i > 0: if i > 0:
if i == 1: RNS.log("Removed "+str(i)+" path state entry", RNS.LOG_EXTREME) if i == 1: RNS.log("Removed "+str(i)+" path state entry", RNS.LOG_EXTREME)
@@ -830,9 +857,13 @@ class Transport:
# Run interface-related jobs # Run interface-related jobs
if time.time() > Transport.interface_last_jobs + Transport.interface_jobs_interval: if time.time() > Transport.interface_last_jobs + Transport.interface_jobs_interval:
Transport.prioritize_interfaces() Transport.prioritize_interfaces()
for interface in Transport.interfaces: try:
interface.process_held_announces() for interface in Transport.interfaces:
Transport.interface_last_jobs = time.time() interface.process_held_announces()
Transport.interface_last_jobs = time.time()
except Exception as e:
RNS.log(f"Error while processing held per-interface announces: {e}", RNS.LOG_WARNING)
RNS.log(f"Postponing until next job run", RNS.LOG_WARNING)
# Clean packet caches # Clean packet caches
if time.time() > Transport.cache_last_cleaned+Transport.cache_clean_interval: if time.time() > Transport.cache_last_cleaned+Transport.cache_clean_interval:
@@ -877,10 +908,6 @@ class Transport:
if should_collect: gc.collect() if should_collect: gc.collect()
else:
# Transport jobs were locked, do nothing
pass
except Exception as e: except Exception as e:
RNS.log("An exception occurred while running Transport jobs.", RNS.LOG_ERROR) RNS.log("An exception occurred while running Transport jobs.", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
@@ -1754,22 +1781,23 @@ class Transport:
# If this is a path response from a local client, # If this is a path response from a local client,
# check if any external interfaces have pending # check if any external interfaces have pending
# path requests. # path requests.
if packet.destination_hash in Transport.pending_local_path_requests: with Transport.pending_local_prs_lock:
desiring_interface = Transport.pending_local_path_requests.pop(packet.destination_hash) if packet.destination_hash in Transport.pending_local_path_requests:
retransmit_timeout = now desiring_interface = Transport.pending_local_path_requests.pop(packet.destination_hash)
retries = Transport.PATHFINDER_R retransmit_timeout = now
retries = Transport.PATHFINDER_R
Transport.announce_table[packet.destination_hash] = [ Transport.announce_table[packet.destination_hash] = [
now, now,
retransmit_timeout, retransmit_timeout,
retries, retries,
received_from, received_from,
announce_hops, announce_hops,
packet, packet,
local_rebroadcasts, local_rebroadcasts,
block_rebroadcasts, block_rebroadcasts,
attached_interface attached_interface
] ]
# If we have any local clients connected, we re- # If we have any local clients connected, we re-
# transmit the announce to them immediately # transmit the announce to them immediately
@@ -2467,44 +2495,45 @@ class Transport:
@staticmethod @staticmethod
def expire_path(destination_hash): def expire_path(destination_hash):
if destination_hash in Transport.path_table: with Transport.path_table_lock:
Transport.path_table[destination_hash][IDX_PT_TIMESTAMP] = 0 if destination_hash in Transport.path_table:
Transport.tables_last_culled = 0 Transport.path_table[destination_hash][IDX_PT_TIMESTAMP] = 0
return True Transport.tables_last_culled = 0
else: return True
return False
else: return False
@staticmethod @staticmethod
def mark_path_unresponsive(destination_hash): def mark_path_unresponsive(destination_hash):
if destination_hash in Transport.path_table: if destination_hash in Transport.path_table:
Transport.path_states[destination_hash] = Transport.STATE_UNRESPONSIVE with Transport.path_states_lock: Transport.path_states[destination_hash] = Transport.STATE_UNRESPONSIVE
return True return True
else:
return False else: return False
@staticmethod @staticmethod
def mark_path_responsive(destination_hash): def mark_path_responsive(destination_hash):
if destination_hash in Transport.path_table: if destination_hash in Transport.path_table:
Transport.path_states[destination_hash] = Transport.STATE_RESPONSIVE with Transport.path_states_lock: Transport.path_states[destination_hash] = Transport.STATE_RESPONSIVE
return True return True
else:
return False else: return False
@staticmethod @staticmethod
def mark_path_unknown_state(destination_hash): def mark_path_unknown_state(destination_hash):
if destination_hash in Transport.path_table: if destination_hash in Transport.path_table:
Transport.path_states[destination_hash] = Transport.STATE_UNKNOWN with Transport.path_states_lock: Transport.path_states[destination_hash] = Transport.STATE_UNKNOWN
return True return True
else:
return False else: return False
@staticmethod @staticmethod
def path_is_unresponsive(destination_hash): def path_is_unresponsive(destination_hash):
if destination_hash in Transport.path_states: with Transport.path_states_lock:
if Transport.path_states[destination_hash] == Transport.STATE_UNRESPONSIVE: if destination_hash in Transport.path_states:
return True if Transport.path_states[destination_hash] == Transport.STATE_UNRESPONSIVE: return True
return False return False
@staticmethod @staticmethod
def await_path(destination_hash, timeout=None, on_interface=None): def await_path(destination_hash, timeout=None, on_interface=None):
@@ -2533,28 +2562,19 @@ class Transport:
:param destination_hash: A destination hash as *bytes*. :param destination_hash: A destination hash as *bytes*.
:param on_interface: If specified, the path request will only be sent on this interface. In normal use, Reticulum handles this automatically, and this parameter should not be used. :param on_interface: If specified, the path request will only be sent on this interface. In normal use, Reticulum handles this automatically, and this parameter should not be used.
""" """
if tag == None: if tag == None: request_tag = RNS.Identity.get_random_hash()
request_tag = RNS.Identity.get_random_hash() else: request_tag = tag
else:
request_tag = tag
if RNS.Reticulum.transport_enabled(): if RNS.Reticulum.transport_enabled(): path_request_data = destination_hash+Transport.identity.hash+request_tag
path_request_data = destination_hash+Transport.identity.hash+request_tag else: path_request_data = destination_hash+request_tag
else:
path_request_data = destination_hash+request_tag
path_request_dst = RNS.Destination(None, RNS.Destination.OUT, RNS.Destination.PLAIN, Transport.APP_NAME, "path", "request") path_request_dst = RNS.Destination(None, RNS.Destination.OUT, RNS.Destination.PLAIN, Transport.APP_NAME, "path", "request")
packet = RNS.Packet(path_request_dst, path_request_data, packet_type = RNS.Packet.DATA, transport_type = RNS.Transport.BROADCAST, header_type = RNS.Packet.HEADER_1, attached_interface = on_interface) packet = RNS.Packet(path_request_dst, path_request_data, packet_type = RNS.Packet.DATA, transport_type = RNS.Transport.BROADCAST, header_type = RNS.Packet.HEADER_1, attached_interface = on_interface)
if on_interface != None and recursive: if on_interface != None and recursive:
if not hasattr(on_interface, "announce_cap"): if not hasattr(on_interface, "announce_cap"): on_interface.announce_cap = RNS.Reticulum.ANNOUNCE_CAP
on_interface.announce_cap = RNS.Reticulum.ANNOUNCE_CAP if not hasattr(on_interface, "announce_allowed_at"): on_interface.announce_allowed_at = 0
if not hasattr(on_interface, "announce_queue"): on_interface.announce_queue = []
if not hasattr(on_interface, "announce_allowed_at"):
on_interface.announce_allowed_at = 0
if not hasattr(on_interface, "announce_queue"):
on_interface.announce_queue = []
queued_announces = True if len(on_interface.announce_queue) > 0 else False queued_announces = True if len(on_interface.announce_queue) > 0 else False
if queued_announces: if queued_announces:
@@ -2581,8 +2601,7 @@ class Transport:
if isinstance(data, list) and len(data) > 0: if isinstance(data, list) and len(data) > 0:
response = [] response = []
response.append(Transport.owner.get_interface_stats()) response.append(Transport.owner.get_interface_stats())
if data[0] == True: if data[0] == True: response.append(Transport.owner.get_link_count())
response.append(Transport.owner.get_link_count())
return response return response
@@ -2601,10 +2620,8 @@ class Transport:
command = data[0] command = data[0]
destination_hash = None destination_hash = None
max_hops = None max_hops = None
if len(data) > 1: if len(data) > 1: destination_hash = data[1]
destination_hash = data[1] if len(data) > 2: max_hops = data[2]
if len(data) > 2:
max_hops = data[2]
if command == "table": if command == "table":
table = Transport.owner.get_path_table(max_hops=max_hops) table = Transport.owner.get_path_table(max_hops=max_hops)
@@ -2658,25 +2675,21 @@ class Transport:
unique_tag = destination_hash+tag_bytes unique_tag = destination_hash+tag_bytes
if not unique_tag in Transport.discovery_pr_tags: with Transport.discovery_pr_tags_lock:
Transport.discovery_pr_tags.append(unique_tag) if not unique_tag in Transport.discovery_pr_tags:
Transport.discovery_pr_tags.append(unique_tag)
Transport.path_request( Transport.path_request(destination_hash,
destination_hash, Transport.from_local_client(packet),
Transport.from_local_client(packet), packet.receiving_interface,
packet.receiving_interface, requestor_transport_id = requesting_transport_instance,
requestor_transport_id = requesting_transport_instance, tag=tag_bytes)
tag=tag_bytes
)
else: else: RNS.log("Ignoring duplicate path request for "+RNS.prettyhexrep(destination_hash)+" with tag "+RNS.prettyhexrep(unique_tag), RNS.LOG_DEBUG)
RNS.log("Ignoring duplicate path request for "+RNS.prettyhexrep(destination_hash)+" with tag "+RNS.prettyhexrep(unique_tag), RNS.LOG_DEBUG)
else: else: RNS.log("Ignoring tagless path request for "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG)
RNS.log("Ignoring tagless path request for "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG)
except Exception as e: except Exception as e: RNS.log("Error while handling path request. The contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("Error while handling path request. The contained exception was: "+str(e), RNS.LOG_ERROR)
@staticmethod @staticmethod
def path_request(destination_hash, is_from_local_client, attached_interface, requestor_transport_id=None, tag=None): def path_request(destination_hash, is_from_local_client, attached_interface, requestor_transport_id=None, tag=None):
@@ -2685,10 +2698,9 @@ class Transport:
if attached_interface != None: if attached_interface != None:
if RNS.Reticulum.transport_enabled() and attached_interface.mode in RNS.Interfaces.Interface.Interface.DISCOVER_PATHS_FOR: if RNS.Reticulum.transport_enabled() and attached_interface.mode in RNS.Interfaces.Interface.Interface.DISCOVER_PATHS_FOR:
should_search_for_unknown = True should_search_for_unknown = True
interface_str = " on "+str(attached_interface) interface_str = " on "+str(attached_interface)
else:
interface_str = "" else: interface_str = ""
RNS.log("Path request for "+RNS.prettyhexrep(destination_hash)+interface_str, RNS.LOG_DEBUG) RNS.log("Path request for "+RNS.prettyhexrep(destination_hash)+interface_str, RNS.LOG_DEBUG)
@@ -2699,7 +2711,8 @@ class Transport:
if Transport.is_local_client_interface(destination_interface): if Transport.is_local_client_interface(destination_interface):
destination_exists_on_local_client = True destination_exists_on_local_client = True
Transport.pending_local_path_requests[destination_hash] = attached_interface with Transport.pending_local_prs_lock:
Transport.pending_local_path_requests[destination_hash] = attached_interface
local_destination = next((d for d in Transport.destinations if d.hash == destination_hash), None) local_destination = next((d for d in Transport.destinations if d.hash == destination_hash), None)
if local_destination != None: if local_destination != None: