Reticulum/RNS/Transport.py

426 lines
15 KiB
Python
Raw Normal View History

2018-04-23 15:42:16 -06:00
import os
import RNS
2018-04-17 09:46:48 -06:00
import time
2019-11-09 15:47:42 -07:00
import math
2018-04-17 09:46:48 -06:00
import threading
2018-04-25 15:05:30 -06:00
import traceback
2018-04-17 09:46:48 -06:00
from time import sleep
2018-04-24 10:01:52 -06:00
import vendor.umsgpack as umsgpack
2016-06-03 11:02:02 -06:00
class Transport:
2018-03-19 09:39:08 -06:00
# Constants
BROADCAST = 0x00;
TRANSPORT = 0x01;
RELAY = 0x02;
TUNNEL = 0x03;
types = [BROADCAST, TRANSPORT, RELAY, TUNNEL]
2019-11-09 15:47:42 -07:00
REACHABILITY_UNREACHABLE = 0x00
REACHABILITY_DIRECT = 0x01
REACHABILITY_TRANSPORT = 0x02
2019-11-10 02:45:52 -07:00
PATHFINDER_M = 18 # Max hops
PATHFINDER_C = 2.0 # Decay constant
PATHFINDER_R = 2 # Retransmit retries
PATHFINDER_T = 10 # Retry grace period
PATHFINDER_E = 60*15 # Path expiration in seconds
interfaces = [] # All active interfaces
destinations = [] # All active destinations
pending_links = [] # Links that are being established
active_links = [] # Links that are active
packet_hashlist = [] # A list of packet hashes for duplicate detection
receipts = [] # Receipts of all outgoing packets for proof processing
2018-04-17 09:46:48 -06:00
2019-11-09 15:47:42 -07:00
announce_table = {}
destination_table = {}
2018-04-17 09:46:48 -06:00
jobs_locked = False
jobs_running = False
job_interval = 0.250
2019-11-09 15:47:42 -07:00
receipts_last_checked = 0.0
receipts_check_interval = 1.0
announces_last_checked = 0.0
announces_check_interval = 1.0
hashlist_maxsize = 1000000
2018-04-17 09:46:48 -06:00
identity = None
2018-04-17 09:46:48 -06:00
@staticmethod
def start():
if Transport.identity == None:
transport_identity_path = RNS.Reticulum.configdir+"/transportidentity"
if os.path.isfile(transport_identity_path):
Transport.identity = RNS.Identity.from_file(transport_identity_path)
if Transport.identity == None:
RNS.log("No valid Transport Identity on disk, creating...", RNS.LOG_VERBOSE)
Transport.identity = RNS.Identity()
Transport.identity.save(transport_identity_path)
else:
RNS.log("Loaded Transport Identity from disk", RNS.LOG_VERBOSE)
2018-04-24 10:01:52 -06:00
packet_hashlist_path = RNS.Reticulum.configdir+"/packet_hashlist"
if os.path.isfile(packet_hashlist_path):
try:
file = open(packet_hashlist_path, "r")
Transport.packet_hashlist = umsgpack.unpackb(file.read())
file.close()
except Exception as e:
RNS.log("Could not load packet hashlist from disk, the contained exception was: "+str(e), RNS.LOG_ERROR)
2018-04-17 09:46:48 -06:00
thread = threading.Thread(target=Transport.jobloop)
thread.setDaemon(True)
thread.start()
RNS.log("Transport instance "+str(Transport.identity)+" started")
2018-04-17 09:46:48 -06:00
@staticmethod
def jobloop():
while (True):
Transport.jobs()
sleep(Transport.job_interval)
@staticmethod
def jobs():
2019-11-09 15:47:42 -07:00
outgoing = []
2018-04-17 09:46:48 -06:00
Transport.jobs_running = True
try:
if not Transport.jobs_locked:
# Process receipts list for timed-out packets
2019-11-09 15:47:42 -07:00
if time.time() > Transport.receipts_last_checked+Transport.receipts_check_interval:
2018-04-17 09:46:48 -06:00
for receipt in Transport.receipts:
2018-04-26 03:29:21 -06:00
thread = threading.Thread(target=receipt.check_timeout)
thread.setDaemon(True)
thread.start()
2018-04-17 09:46:48 -06:00
if receipt.status != RNS.PacketReceipt.SENT:
Transport.receipts.remove(receipt)
Transport.receipts_last_checked = time.time()
2019-11-09 15:47:42 -07:00
# Process announces needing retransmission
if time.time() > Transport.announces_last_checked+Transport.announces_check_interval:
for destination_hash in Transport.announce_table:
announce_entry = Transport.announce_table[destination_hash]
# TODO: remove comment and log output
# [time_heard, retransmit_timeout, retries, received_from, packet.hops, packet]
# RNS.log("Announce entry retries: "+str(announce_entry[2]), RNS.LOG_INFO)
# RNS.log("Max retries: "+str(Transport.PATHFINDER_R), RNS.LOG_INFO)
if announce_entry[2] > Transport.PATHFINDER_R:
2019-11-10 02:45:52 -07:00
RNS.log("Dropping announce for "+RNS.prettyhexrep(destination_hash)+", retries exceeded", RNS.LOG_DEBUG)
2019-11-09 15:47:42 -07:00
Transport.announce_table.pop(destination_hash)
break
else:
if time.time() > announce_entry[1]:
# RNS.log("Rebroadcasting announce", RNS.LOG_INFO)
announce_entry[1] = time.time() + math.pow(Transport.PATHFINDER_C, announce_entry[4]) + Transport.PATHFINDER_T
announce_entry[2] += 1
packet = announce_entry[5]
announce_data = packet.data
announce_identity = RNS.Identity.recall(packet.destination_hash)
announce_destination = RNS.Destination(announce_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "unknown", "unknown");
announce_destination.hash = packet.destination_hash
announce_destination.hexhash = announce_destination.hash.encode("hex_codec")
new_packet = RNS.Packet(announce_destination, announce_data, RNS.Packet.ANNOUNCE, header_type = RNS.Packet.HEADER_2, transport_type = Transport.TRANSPORT, transport_id = Transport.identity.hash)
new_packet.hops = announce_entry[4]
2019-11-10 02:45:52 -07:00
RNS.log("Rebroadcasting announce for "+RNS.prettyhexrep(announce_destination.hash)+" with hop count "+str(new_packet.hops), RNS.LOG_DEBUG)
2019-11-09 15:47:42 -07:00
outgoing.append(new_packet)
Transport.announces_last_checked = time.time()
2018-04-17 09:46:48 -06:00
# Cull the packet hashlist if it has reached max size
while (len(Transport.packet_hashlist) > Transport.hashlist_maxsize):
Transport.packet_hashlist.pop(0)
except Exception as e:
RNS.log("An exception occurred while running Transport jobs.", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
2018-04-25 15:05:30 -06:00
traceback.print_exc()
2018-04-17 09:46:48 -06:00
Transport.jobs_running = False
2018-03-19 13:51:26 -06:00
2019-11-09 15:47:42 -07:00
for packet in outgoing:
packet.send()
2016-06-03 11:02:02 -06:00
@staticmethod
2018-04-16 09:13:39 -06:00
def outbound(packet):
2018-04-17 09:46:48 -06:00
while (Transport.jobs_running):
2018-04-23 15:42:16 -06:00
sleep(0.01)
2018-04-17 09:46:48 -06:00
Transport.jobs_locked = True
packet.updateHash()
sent = False
2018-03-20 05:32:41 -06:00
for interface in Transport.interfaces:
if interface.OUT:
2018-04-16 09:13:39 -06:00
should_transmit = True
if packet.destination.type == RNS.Destination.LINK:
if packet.destination.status == RNS.Link.CLOSED:
should_transmit = False
2018-04-16 09:13:39 -06:00
if interface != packet.destination.attached_interface:
should_transmit = False
if should_transmit:
2018-04-26 05:20:43 -06:00
# TODO: Remove
RNS.log("Transmitting "+str(len(packet.raw))+" bytes via: "+str(interface), RNS.LOG_EXTREME)
RNS.log("Hash is "+RNS.prettyhexrep(packet.packet_hash), RNS.LOG_EXTREME)
2018-04-16 09:13:39 -06:00
interface.processOutgoing(packet.raw)
2018-04-17 09:46:48 -06:00
sent = True
if sent:
packet.sent = True
packet.sent_at = time.time()
if (packet.packet_type == RNS.Packet.DATA):
packet.receipt = RNS.PacketReceipt(packet)
Transport.receipts.append(packet.receipt)
Transport.cache(packet)
Transport.jobs_locked = False
return sent
2016-06-03 11:02:02 -06:00
@staticmethod
def packet_filter(packet):
# TODO: Think long and hard about this
if packet.context == RNS.Packet.KEEPALIVE:
return True
2018-04-23 15:42:16 -06:00
if packet.context == RNS.Packet.RESOURCE_REQ:
return True
if packet.context == RNS.Packet.RESOURCE_PRF:
return True
if not packet.packet_hash in Transport.packet_hashlist:
return True
2018-04-23 15:42:16 -06:00
return False
2018-03-19 13:51:26 -06:00
@staticmethod
def inbound(raw, interface=None):
2018-04-17 09:46:48 -06:00
while (Transport.jobs_running):
sleep(0.1)
Transport.jobs_locked = True
packet = RNS.Packet(None, raw)
packet.unpack()
packet.updateHash()
packet.receiving_interface = interface
2018-03-19 13:51:26 -06:00
2018-04-23 15:42:16 -06:00
RNS.log(str(interface)+" received packet with hash "+RNS.prettyhexrep(packet.packet_hash), RNS.LOG_EXTREME)
2018-03-19 13:51:26 -06:00
2018-04-23 15:42:16 -06:00
# TODO: Rewrite these redundant cache calls
if Transport.packet_filter(packet):
2018-04-17 09:46:48 -06:00
Transport.packet_hashlist.append(packet.packet_hash)
if packet.packet_type == RNS.Packet.ANNOUNCE:
if RNS.Identity.validateAnnounce(packet):
2019-11-09 15:47:42 -07:00
if (packet.transport_id != None):
received_from = packet.transport_id
# Check if this is a next retransmission from
# another node. If it is, we're removing the
# announcein question from our pending table
if packet.destination_hash in Transport.announce_table:
announce_entry = Transport.announce_table[packet.destination_hash]
if packet.hops == announce_entry[4]+1 and announce_entry[2] > 0:
now = time.time()
2019-11-10 02:45:52 -07:00
if now < announce_entry[1]:
Transport.announce_table.pop(packet.destination_hash)
2019-11-09 15:47:42 -07:00
else:
received_from = packet.destination_hash
2019-11-10 02:45:52 -07:00
# Check if this announce should be inserted into
# announce and destination tables
should_add = False
2019-11-09 15:47:42 -07:00
packet.hops += 1
2019-11-10 02:45:52 -07:00
if (not any(packet.destination_hash == d.hash for d in Transport.destinations) and packet.hops < Transport.PATHFINDER_M+1):
if packet.destination_hash in Transport.destination_table:
if packet.hops <= Transport.destination_table[packet.destination_hash][2]:
# If we already have a path to the announced
# destination, but the hop count is equal or
# less, we'll update our tables.
should_add = True
else:
# If an announce arrives with a larger hop
# count than we already have in the table,
# ignore it, unless the path is expired
if (time.time() < Transport.destination_table[packet.destination_hash][3]):
RNS.log("Replacing destination table entry for "+str(RNS.prettyhexrep(packet.destination_hash))+" with new announce due to expired path", RNS.LOG_DEBUG)
should_add = True
else:
should_add = False
else:
# If this destination is unknown in our table
# we should add it
should_add = True
if should_add:
now = time.time()
retries = 0
expires = now + Transport.PATHFINDER_E
retransmit_timeout = now + math.pow(Transport.PATHFINDER_C, packet.hops)
Transport.announce_table[packet.destination_hash] = [now, retransmit_timeout, retries, received_from, packet.hops, packet]
Transport.destination_table[packet.destination_hash] = [now, received_from, packet.hops, expires]
2018-04-18 15:31:17 -06:00
elif packet.packet_type == RNS.Packet.LINKREQUEST:
2018-03-20 05:32:41 -06:00
for destination in Transport.destinations:
2018-03-19 13:51:26 -06:00
if destination.hash == packet.destination_hash and destination.type == packet.destination_type:
2018-03-20 05:32:41 -06:00
packet.destination = destination
destination.receive(packet)
Transport.cache(packet)
2018-04-16 09:13:39 -06:00
2018-04-18 15:31:17 -06:00
elif packet.packet_type == RNS.Packet.DATA:
2018-04-16 09:13:39 -06:00
if packet.destination_type == RNS.Destination.LINK:
for link in Transport.active_links:
if link.link_id == packet.destination_hash:
2018-04-18 15:31:17 -06:00
packet.link = link
2018-04-16 09:13:39 -06:00
link.receive(packet)
Transport.cache(packet)
else:
for destination in Transport.destinations:
if destination.hash == packet.destination_hash and destination.type == packet.destination_type:
packet.destination = destination
destination.receive(packet)
Transport.cache(packet)
2018-03-20 05:32:41 -06:00
2018-04-17 09:46:48 -06:00
if destination.proof_strategy == RNS.Destination.PROVE_ALL:
packet.prove()
elif destination.proof_strategy == RNS.Destination.PROVE_APP:
2018-04-17 09:46:48 -06:00
if destination.callbacks.proof_requested:
if destination.callbacks.proof_requested(packet):
packet.prove()
2018-04-17 09:46:48 -06:00
2018-04-18 15:31:17 -06:00
elif packet.packet_type == RNS.Packet.PROOF:
if packet.context == RNS.Packet.LRPROOF:
2018-04-16 09:13:39 -06:00
# This is a link request proof, forward
# to a waiting link request
for link in Transport.pending_links:
if link.link_id == packet.destination_hash:
link.validateProof(packet)
2018-04-18 15:31:17 -06:00
elif packet.context == RNS.Packet.RESOURCE_PRF:
for link in Transport.active_links:
if link.link_id == packet.destination_hash:
link.receive(packet)
2018-04-16 09:13:39 -06:00
else:
if packet.destination_type == RNS.Destination.LINK:
for link in Transport.active_links:
if link.link_id == packet.destination_hash:
packet.link = link
# plaintext = link.decrypt(packet.data)
2018-04-17 09:46:48 -06:00
# TODO: Make sure everything uses new proof handling
if len(packet.data) == RNS.PacketReceipt.EXPL_LENGTH:
proof_hash = packet.data[:RNS.Identity.HASHLENGTH/8]
else:
proof_hash = None
for receipt in Transport.receipts:
receipt_validated = False
if proof_hash != None:
# Only test validation if hash matches
if receipt.hash == proof_hash:
receipt_validated = receipt.validateProofPacket(packet)
else:
# In case of an implicit proof, we have
# to check every single outstanding receipt
receipt_validated = receipt.validateProofPacket(packet)
if receipt_validated:
Transport.receipts.remove(receipt)
Transport.jobs_locked = False
2018-03-19 13:51:26 -06:00
2016-06-03 11:02:02 -06:00
@staticmethod
def registerDestination(destination):
destination.MTU = RNS.Reticulum.MTU
if destination.direction == RNS.Destination.IN:
2018-03-20 05:32:41 -06:00
Transport.destinations.append(destination)
2018-04-16 09:13:39 -06:00
@staticmethod
def registerLink(link):
RNS.log("Registering link "+str(link), RNS.LOG_DEBUG)
2018-04-16 09:13:39 -06:00
if link.initiator:
Transport.pending_links.append(link)
else:
Transport.active_links.append(link)
@staticmethod
def activateLink(link):
RNS.log("Activating link "+str(link), RNS.LOG_DEBUG)
2018-04-16 09:13:39 -06:00
if link in Transport.pending_links:
Transport.pending_links.remove(link)
Transport.active_links.append(link)
link.status = RNS.Link.ACTIVE
else:
RNS.log("Attempted to activate a link that was not in the pending table", RNS.LOG_ERROR)
@staticmethod
def shouldCache(packet):
# TODO: Implement sensible rules for which
# packets to cache
2018-04-23 15:42:16 -06:00
if packet.context == RNS.Packet.RESOURCE_PRF:
return True
2018-04-16 09:13:39 -06:00
return False
2018-03-20 05:32:41 -06:00
@staticmethod
def cache(packet):
2018-04-16 09:13:39 -06:00
if RNS.Transport.shouldCache(packet):
2018-04-17 09:46:48 -06:00
try:
packet_hash = RNS.hexrep(packet.getHash(), delimit=False)
file = open(RNS.Reticulum.cachepath+"/"+packet_hash, "w")
file.write(packet.raw)
file.close()
2018-04-23 15:42:16 -06:00
RNS.log("Wrote packet "+packet_hash+" to cache", RNS.LOG_EXTREME)
2018-04-17 09:46:48 -06:00
except Exception as e:
RNS.log("Error writing packet to cache", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e))
2018-03-20 05:32:41 -06:00
2018-04-23 15:42:16 -06:00
@staticmethod
def cache_request_packet(packet):
if len(packet.data) == RNS.Identity.HASHLENGTH/8:
packet_hash = RNS.hexrep(packet.data, delimit=False)
2019-11-09 15:47:42 -07:00
# TODO: There's some pretty obvious file access
# issues here. Make sure this can't happen
2018-04-23 15:42:16 -06:00
path = RNS.Reticulum.cachepath+"/"+packet_hash
if os.path.isfile(path):
file = open(path, "r")
raw = file.read()
file.close()
packet = RNS.Packet(None, raw)
# TODO: Implement outbound for this
@staticmethod
def cache_request(packet_hash):
RNS.log("Cache request for "+RNS.prettyhexrep(packet_hash), RNS.LOG_EXTREME)
path = RNS.Reticulum.cachepath+"/"+RNS.hexrep(packet_hash, delimit=False)
if os.path.isfile(path):
file = open(path, "r")
raw = file.read()
Transport.inbound(raw)
file.close()
else:
cache_request_packet = RNS.Packet(Transport.transport_destination(), packet_hash, context = RNS.Packet.CACHE_REQUEST)
@staticmethod
def transport_destination():
# TODO: implement this
pass
2018-04-24 10:01:52 -06:00
@staticmethod
def exitHandler():
try:
packet_hashlist_path = RNS.Reticulum.configdir+"/packet_hashlist"
file = open(packet_hashlist_path, "w")
file.write(umsgpack.packb(Transport.packet_hashlist))
file.close()
except Exception as e:
RNS.log("Could not save packet hashlist to disk, the contained exception was: "+str(e), RNS.LOG_ERROR)