2022-04-01 09:18:18 -06:00
# MIT License
#
2024-09-04 09:37:18 -06:00
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors.
2022-04-01 09:18:18 -06:00
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
2022-06-08 11:47:09 -06:00
from RNS . Cryptography import X25519PrivateKey , X25519PublicKey , Ed25519PrivateKey , Ed25519PublicKey
2024-11-22 07:19:12 -07:00
from RNS . Cryptography import Token
2023-02-25 17:23:25 -07:00
from RNS . Channel import Channel , LinkChannelOutlet
2023-02-27 20:33:50 -07:00
2018-04-21 08:12:42 -06:00
from time import sleep
2020-04-22 04:07:13 -06:00
from . vendor import umsgpack as umsgpack
2018-04-21 08:12:42 -06:00
import threading
2023-02-09 03:52:54 -07:00
import inspect
2020-04-28 12:50:57 -06:00
import math
2018-04-21 05:54:22 -06:00
import time
2018-04-16 09:13:39 -06:00
import RNS
2022-03-07 16:38:51 -07:00
2018-04-16 09:13:39 -06:00
class LinkCallbacks :
2020-08-13 04:15:56 -06:00
def __init__ ( self ) :
self . link_established = None
self . link_closed = None
self . packet = None
2021-05-16 15:14:49 -06:00
self . resource = None
2020-08-13 04:15:56 -06:00
self . resource_started = None
self . resource_concluded = None
2021-08-19 06:10:37 -06:00
self . remote_identified = None
2018-04-16 09:13:39 -06:00
class Link :
2021-05-16 15:14:49 -06:00
"""
2021-09-02 12:35:42 -06:00
This class is used to establish and manage links to other peers . When a
link instance is created , Reticulum will attempt to establish verified
2022-09-30 03:26:51 -06:00
and encrypted connectivity with the specified destination .
2021-05-16 15:14:49 -06:00
: param destination : A : ref : ` RNS . Destination < api - destination > ` instance which to establish a link to .
2021-09-02 12:35:42 -06:00
: param established_callback : An optional function or method with the signature * callback ( link ) * to be called when the link has been established .
: param closed_callback : An optional function or method with the signature * callback ( link ) * to be called when the link is closed .
2021-05-16 15:14:49 -06:00
"""
2021-05-20 07:31:38 -06:00
CURVE = RNS . Identity . CURVE
2021-05-16 15:14:49 -06:00
"""
The curve used for Elliptic Curve DH key exchanges
"""
2021-05-20 07:31:38 -06:00
ECPUBSIZE = 32 + 32
KEYSIZE = 32
2021-05-17 06:05:13 -06:00
2024-11-22 07:19:12 -07:00
MDU = math . floor ( ( RNS . Reticulum . MTU - RNS . Reticulum . IFAC_MIN_SIZE - RNS . Reticulum . HEADER_MINSIZE - RNS . Identity . TOKEN_OVERHEAD ) / RNS . Identity . AES128_BLOCKSIZE ) * RNS . Identity . AES128_BLOCKSIZE - 1
2020-08-13 04:15:56 -06:00
2021-12-05 03:45:13 -07:00
ESTABLISHMENT_TIMEOUT_PER_HOP = RNS . Reticulum . DEFAULT_PER_HOP_TIMEOUT
2021-05-16 15:14:49 -06:00
"""
2022-05-26 08:49:02 -06:00
Timeout for link establishment in seconds per hop to destination .
2021-05-16 15:14:49 -06:00
"""
2021-08-28 12:01:01 -06:00
2024-09-12 09:52:40 -06:00
TRAFFIC_TIMEOUT_MIN_MS = 5
2021-09-03 06:22:53 -06:00
TRAFFIC_TIMEOUT_FACTOR = 6
2021-08-28 12:01:01 -06:00
KEEPALIVE_TIMEOUT_FACTOR = 4
2022-05-26 08:49:02 -06:00
"""
RTT timeout factor used in link timeout calculation .
"""
2020-08-13 04:15:56 -06:00
STALE_GRACE = 2
2022-05-26 08:49:02 -06:00
"""
Grace period in seconds used in link timeout calculation .
"""
2022-05-26 08:52:28 -06:00
KEEPALIVE = 360
2021-05-16 15:14:49 -06:00
"""
Interval for sending keep - alive packets on established links in seconds .
"""
2022-05-26 08:49:02 -06:00
STALE_TIME = 2 * KEEPALIVE
"""
If no traffic or keep - alive packets are received within this period , the
link will be marked as stale , and a final keep - alive packet will be sent .
If after this no traffic or keep - alive packets are received within ` ` RTT ` ` *
` ` KEEPALIVE_TIMEOUT_FACTOR ` ` + ` ` STALE_GRACE ` ` , the link is considered timed out ,
and will be torn down .
"""
2020-08-13 04:15:56 -06:00
PENDING = 0x00
HANDSHAKE = 0x01
ACTIVE = 0x02
STALE = 0x03
CLOSED = 0x04
TIMEOUT = 0x01
INITIATOR_CLOSED = 0x02
DESTINATION_CLOSED = 0x03
ACCEPT_NONE = 0x00
ACCEPT_APP = 0x01
ACCEPT_ALL = 0x02
resource_strategies = [ ACCEPT_NONE , ACCEPT_APP , ACCEPT_ALL ]
@staticmethod
2021-05-16 08:37:12 -06:00
def validate_request ( owner , data , packet ) :
2020-08-13 04:15:56 -06:00
if len ( data ) == ( Link . ECPUBSIZE ) :
try :
2021-05-17 11:11:10 -06:00
link = Link ( owner = owner , peer_pub_bytes = data [ : Link . ECPUBSIZE / / 2 ] , peer_sig_pub_bytes = data [ Link . ECPUBSIZE / / 2 : Link . ECPUBSIZE ] )
2021-05-16 08:37:12 -06:00
link . set_link_id ( packet )
2020-08-13 04:15:56 -06:00
link . destination = packet . destination
2023-10-27 10:16:52 -06:00
link . establishment_timeout = Link . ESTABLISHMENT_TIMEOUT_PER_HOP * max ( 1 , packet . hops ) + Link . KEEPALIVE
2022-06-09 11:28:31 -06:00
link . establishment_cost + = len ( packet . raw )
2020-08-13 04:15:56 -06:00
RNS . log ( " Validating link request " + RNS . prettyhexrep ( link . link_id ) , RNS . LOG_VERBOSE )
2023-10-27 10:16:52 -06:00
RNS . log ( f " Establishment timeout is { RNS . prettytime ( link . establishment_timeout ) } for incoming link request " + RNS . prettyhexrep ( link . link_id ) , RNS . LOG_EXTREME )
2020-08-13 04:15:56 -06:00
link . handshake ( )
link . attached_interface = packet . receiving_interface
link . prove ( )
link . request_time = time . time ( )
2021-05-16 08:48:54 -06:00
RNS . Transport . register_link ( link )
2020-08-13 04:15:56 -06:00
link . last_inbound = time . time ( )
2024-10-20 05:34:02 -06:00
link . __update_phy_stats ( packet , force_update = True )
2020-08-13 04:15:56 -06:00
link . start_watchdog ( )
2024-10-20 05:34:02 -06:00
2024-09-21 11:04:28 -06:00
RNS . log ( " Incoming link request " + str ( link ) + " accepted on " + str ( link . attached_interface ) , RNS . LOG_DEBUG )
2020-08-13 04:15:56 -06:00
return link
except Exception as e :
2024-10-20 05:34:02 -06:00
RNS . log ( f " Validating link request failed: { e } " , RNS . LOG_VERBOSE )
2020-08-13 04:15:56 -06:00
return None
else :
2024-02-29 15:05:16 -07:00
RNS . log ( " Invalid link request payload size, dropping request " , RNS . LOG_DEBUG )
2020-08-13 04:15:56 -06:00
return None
2021-08-27 11:52:48 -06:00
def __init__ ( self , destination = None , established_callback = None , closed_callback = None , owner = None , peer_pub_bytes = None , peer_sig_pub_bytes = None ) :
2020-08-13 04:15:56 -06:00
if destination != None and destination . type != RNS . Destination . SINGLE :
raise TypeError ( " Links can only be established to the \" single \" destination type " )
self . rtt = None
2022-06-09 11:28:31 -06:00
self . establishment_cost = 0
2024-09-04 02:32:54 -06:00
self . establishment_rate = None
2020-08-13 04:15:56 -06:00
self . callbacks = LinkCallbacks ( )
self . resource_strategy = Link . ACCEPT_NONE
self . outgoing_resources = [ ]
self . incoming_resources = [ ]
2021-08-20 15:29:06 -06:00
self . pending_requests = [ ]
2020-08-13 04:15:56 -06:00
self . last_inbound = 0
self . last_outbound = 0
2023-05-11 08:22:44 -06:00
self . last_proof = 0
2024-02-29 15:05:16 -07:00
self . last_data = 0
2020-08-13 04:15:56 -06:00
self . tx = 0
self . rx = 0
self . txbytes = 0
self . rxbytes = 0
2023-10-23 17:41:12 -06:00
self . rssi = None
self . snr = None
self . q = None
2021-08-28 12:01:01 -06:00
self . traffic_timeout_factor = Link . TRAFFIC_TIMEOUT_FACTOR
self . keepalive_timeout_factor = Link . KEEPALIVE_TIMEOUT_FACTOR
2020-08-13 04:15:56 -06:00
self . keepalive = Link . KEEPALIVE
2022-05-26 08:49:02 -06:00
self . stale_time = Link . STALE_TIME
2020-08-13 04:15:56 -06:00
self . watchdog_lock = False
self . status = Link . PENDING
2021-12-11 09:26:45 -07:00
self . activated_at = None
2020-08-13 04:15:56 -06:00
self . type = RNS . Destination . LINK
self . owner = owner
self . destination = destination
2024-05-04 11:15:57 -06:00
self . expected_hops = None
2020-08-13 04:15:56 -06:00
self . attached_interface = None
2021-08-19 06:10:37 -06:00
self . __remote_identity = None
2023-10-27 16:05:35 -06:00
self . __track_phy_stats = False
2023-02-25 17:23:25 -07:00
self . _channel = None
2024-05-04 11:15:57 -06:00
2020-08-13 04:15:56 -06:00
if self . destination == None :
self . initiator = False
2022-09-29 14:47:10 -06:00
self . prv = X25519PrivateKey . generate ( )
2021-05-20 12:32:08 -06:00
self . sig_prv = self . owner . identity . sig_prv
2020-08-13 04:15:56 -06:00
else :
self . initiator = True
2024-05-04 11:15:57 -06:00
self . expected_hops = RNS . Transport . hops_to ( self . destination . hash )
2023-10-27 10:16:52 -06:00
self . establishment_timeout = RNS . Reticulum . get_instance ( ) . get_first_hop_timeout ( destination . hash )
self . establishment_timeout + = Link . ESTABLISHMENT_TIMEOUT_PER_HOP * max ( 1 , RNS . Transport . hops_to ( destination . hash ) )
2021-05-20 12:32:08 -06:00
self . prv = X25519PrivateKey . generate ( )
self . sig_prv = Ed25519PrivateKey . generate ( )
2021-05-17 06:05:13 -06:00
2024-11-22 07:19:12 -07:00
self . token = None
2020-08-13 04:15:56 -06:00
self . pub = self . prv . public_key ( )
2022-06-08 05:36:23 -06:00
self . pub_bytes = self . pub . public_bytes ( )
2020-08-13 04:15:56 -06:00
2021-05-17 11:11:10 -06:00
self . sig_pub = self . sig_prv . public_key ( )
2022-06-08 11:47:09 -06:00
self . sig_pub_bytes = self . sig_pub . public_bytes ( )
2021-05-17 11:11:10 -06:00
2020-08-13 04:15:56 -06:00
if peer_pub_bytes == None :
self . peer_pub = None
self . peer_pub_bytes = None
else :
2021-05-17 11:11:10 -06:00
self . load_peer ( peer_pub_bytes , peer_sig_pub_bytes )
2020-08-13 04:15:56 -06:00
2021-08-27 11:52:48 -06:00
if established_callback != None :
self . set_link_established_callback ( established_callback )
if closed_callback != None :
self . set_link_closed_callback ( closed_callback )
2020-08-13 04:15:56 -06:00
if ( self . initiator ) :
2021-05-17 11:11:10 -06:00
self . request_data = self . pub_bytes + self . sig_pub_bytes
2020-08-13 04:15:56 -06:00
self . packet = RNS . Packet ( destination , self . request_data , packet_type = RNS . Packet . LINKREQUEST )
self . packet . pack ( )
2022-06-09 11:28:31 -06:00
self . establishment_cost + = len ( self . packet . raw )
2021-05-16 08:37:12 -06:00
self . set_link_id ( self . packet )
2021-05-16 08:48:54 -06:00
RNS . Transport . register_link ( self )
2020-08-13 04:15:56 -06:00
self . request_time = time . time ( )
self . start_watchdog ( )
self . packet . send ( )
2021-05-14 04:58:02 -06:00
self . had_outbound ( )
2021-10-08 09:06:00 -06:00
RNS . log ( " Link request " + RNS . prettyhexrep ( self . link_id ) + " sent to " + str ( self . destination ) , RNS . LOG_DEBUG )
2023-10-27 10:16:52 -06:00
RNS . log ( f " Establishment timeout is { RNS . prettytime ( self . establishment_timeout ) } for link request " + RNS . prettyhexrep ( self . link_id ) , RNS . LOG_EXTREME )
2020-08-13 04:15:56 -06:00
2021-05-17 11:11:10 -06:00
def load_peer ( self , peer_pub_bytes , peer_sig_pub_bytes ) :
2020-08-13 04:15:56 -06:00
self . peer_pub_bytes = peer_pub_bytes
2021-05-17 06:05:13 -06:00
self . peer_pub = X25519PublicKey . from_public_bytes ( self . peer_pub_bytes )
2021-05-17 11:11:10 -06:00
self . peer_sig_pub_bytes = peer_sig_pub_bytes
2021-05-17 06:05:13 -06:00
self . peer_sig_pub = Ed25519PublicKey . from_public_bytes ( self . peer_sig_pub_bytes )
2020-08-13 04:15:56 -06:00
if not hasattr ( self . peer_pub , " curve " ) :
self . peer_pub . curve = Link . CURVE
2021-05-16 08:37:12 -06:00
def set_link_id ( self , packet ) :
2020-08-13 04:15:56 -06:00
self . link_id = packet . getTruncatedHash ( )
self . hash = self . link_id
def handshake ( self ) :
2023-06-02 13:24:49 -06:00
if self . status == Link . PENDING and self . prv != None :
self . status = Link . HANDSHAKE
self . shared_key = self . prv . exchange ( self . peer_pub )
self . derived_key = RNS . Cryptography . hkdf (
length = 32 ,
derive_from = self . shared_key ,
salt = self . get_salt ( ) ,
context = self . get_context ( ) ,
)
else :
RNS . log ( " Handshake attempt on " + str ( self ) + " with invalid state " + str ( self . status ) , RNS . LOG_ERROR )
2022-06-07 07:48:23 -06:00
2020-08-13 04:15:56 -06:00
def prove ( self ) :
2021-05-17 11:11:10 -06:00
signed_data = self . link_id + self . pub_bytes + self . sig_pub_bytes
2020-08-13 04:15:56 -06:00
signature = self . owner . identity . sign ( signed_data )
2022-09-29 14:47:10 -06:00
proof_data = signature + self . pub_bytes
2020-08-13 04:15:56 -06:00
proof = RNS . Packet ( self , proof_data , packet_type = RNS . Packet . PROOF , context = RNS . Packet . LRPROOF )
proof . send ( )
2022-06-09 11:28:31 -06:00
self . establishment_cost + = len ( proof . raw )
2021-05-14 04:58:02 -06:00
self . had_outbound ( )
2020-08-13 04:15:56 -06:00
2021-12-11 10:19:51 -07:00
2020-08-13 04:15:56 -06:00
def prove_packet ( self , packet ) :
signature = self . sign ( packet . packet_hash )
# TODO: Hardcoded as explicit proof for now
# if RNS.Reticulum.should_use_implicit_proof():
# proof_data = signature
# else:
# proof_data = packet.packet_hash + signature
proof_data = packet . packet_hash + signature
proof = RNS . Packet ( self , proof_data , RNS . Packet . PROOF )
proof . send ( )
2021-05-14 04:58:02 -06:00
self . had_outbound ( )
2020-08-13 04:15:56 -06:00
2021-05-16 08:37:12 -06:00
def validate_proof ( self , packet ) :
2023-06-13 08:10:47 -06:00
try :
if self . status == Link . PENDING :
if self . initiator and len ( packet . data ) == RNS . Identity . SIGLENGTH / / 8 + Link . ECPUBSIZE / / 2 :
peer_pub_bytes = packet . data [ RNS . Identity . SIGLENGTH / / 8 : RNS . Identity . SIGLENGTH / / 8 + Link . ECPUBSIZE / / 2 ]
peer_sig_pub_bytes = self . destination . identity . get_public_key ( ) [ Link . ECPUBSIZE / / 2 : Link . ECPUBSIZE ]
self . load_peer ( peer_pub_bytes , peer_sig_pub_bytes )
self . handshake ( )
self . establishment_cost + = len ( packet . raw )
signed_data = self . link_id + self . peer_pub_bytes + self . peer_sig_pub_bytes
signature = packet . data [ : RNS . Identity . SIGLENGTH / / 8 ]
2023-02-17 01:54:18 -07:00
2023-06-13 08:10:47 -06:00
if self . destination . identity . validate ( signature , signed_data ) :
2023-06-13 11:20:54 -06:00
if self . status != Link . HANDSHAKE :
raise IOError ( " Invalid link state for proof validation: " + str ( self . status ) )
2023-06-13 08:10:47 -06:00
self . rtt = time . time ( ) - self . request_time
self . attached_interface = packet . receiving_interface
self . __remote_identity = self . destination . identity
self . status = Link . ACTIVE
self . activated_at = time . time ( )
self . last_proof = self . activated_at
RNS . Transport . activate_link ( self )
RNS . log ( " Link " + str ( self ) + " established with " + str ( self . destination ) + " , RTT is " + str ( round ( self . rtt , 3 ) ) + " s " , RNS . LOG_VERBOSE )
if self . rtt != None and self . establishment_cost != None and self . rtt > 0 and self . establishment_cost > 0 :
self . establishment_rate = self . establishment_cost / self . rtt
rtt_data = umsgpack . packb ( self . rtt )
rtt_packet = RNS . Packet ( self , rtt_data , context = RNS . Packet . LRRTT )
rtt_packet . send ( )
self . had_outbound ( )
2024-10-20 05:34:02 -06:00
self . __update_phy_stats ( packet )
2023-06-13 08:10:47 -06:00
if self . callbacks . link_established != None :
thread = threading . Thread ( target = self . callbacks . link_established , args = ( self , ) )
thread . daemon = True
thread . start ( )
else :
RNS . log ( " Invalid link proof signature received by " + str ( self ) + " . Ignoring. " , RNS . LOG_DEBUG )
except Exception as e :
self . status = Link . CLOSED
RNS . log ( " An error ocurred while validating link request proof on " + str ( self ) + " . " , RNS . LOG_ERROR )
RNS . log ( " The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2020-08-13 04:15:56 -06:00
2021-08-19 06:10:37 -06:00
def identify ( self , identity ) :
"""
Identifies the initiator of the link to the remote peer . This can only happen
once the link has been established , and is carried out over the encrypted link .
The identity is only revealed to the remote peer , and initiator anonymity is
thus preserved . This method can be used for authentication .
: param identity : An RNS . Identity instance to identify as .
"""
2023-09-21 06:12:32 -06:00
if self . initiator and self . status == Link . ACTIVE :
2021-08-19 06:10:37 -06:00
signed_data = self . link_id + identity . get_public_key ( )
signature = identity . sign ( signed_data )
proof_data = identity . get_public_key ( ) + signature
proof = RNS . Packet ( self , proof_data , RNS . Packet . DATA , context = RNS . Packet . LINKIDENTIFY )
proof . send ( )
self . had_outbound ( )
2021-08-28 12:01:01 -06:00
def request ( self , path , data = None , response_callback = None , failed_callback = None , progress_callback = None , timeout = None ) :
2021-08-20 15:29:06 -06:00
"""
Sends a request to the remote peer .
: param path : The request path .
2021-08-27 11:52:48 -06:00
: param response_callback : An optional function or method with the signature * response_callback ( request_receipt ) * to be called when a response is received . See the : ref : ` Request Example < example - request > ` for more info .
: param failed_callback : An optional function or method with the signature * failed_callback ( request_receipt ) * to be called when a request fails . See the : ref : ` Request Example < example - request > ` for more info .
2021-08-28 12:01:01 -06:00
: param progress_callback : An optional function or method with the signature * progress_callback ( request_receipt ) * to be called when progress is made receiving the response . Progress can be accessed as a float between 0.0 and 1.0 by the * request_receipt . progress * property .
: param timeout : An optional timeout in seconds for the request . If * None * is supplied it will be calculated based on link RTT .
2021-09-02 10:00:03 -06:00
: returns : A : ref : ` RNS . RequestReceipt < api - requestreceipt > ` instance if the request was sent , or * False * if it was not .
2021-08-20 15:29:06 -06:00
"""
request_path_hash = RNS . Identity . truncated_hash ( path . encode ( " utf-8 " ) )
unpacked_request = [ time . time ( ) , request_path_hash , data ]
packed_request = umsgpack . packb ( unpacked_request )
2021-08-28 12:01:01 -06:00
if timeout == None :
2024-01-13 17:06:43 -07:00
timeout = self . rtt * self . traffic_timeout_factor + RNS . Resource . RESPONSE_MAX_GRACE_TIME * 1.125
2021-08-28 12:01:01 -06:00
2021-08-20 15:29:06 -06:00
if len ( packed_request ) < = Link . MDU :
request_packet = RNS . Packet ( self , packed_request , RNS . Packet . DATA , context = RNS . Packet . REQUEST )
2021-08-27 11:52:48 -06:00
packet_receipt = request_packet . send ( )
2021-09-02 10:00:03 -06:00
if packet_receipt == False :
return False
else :
packet_receipt . set_timeout ( timeout )
return RequestReceipt (
self ,
packet_receipt = packet_receipt ,
response_callback = response_callback ,
failed_callback = failed_callback ,
progress_callback = progress_callback ,
2022-05-24 12:13:11 -06:00
timeout = timeout ,
request_size = len ( packed_request ) ,
2021-09-02 10:00:03 -06:00
)
2021-08-20 15:29:06 -06:00
else :
2021-08-21 06:52:31 -06:00
request_id = RNS . Identity . truncated_hash ( packed_request )
RNS . log ( " Sending request " + RNS . prettyhexrep ( request_id ) + " as resource. " , RNS . LOG_DEBUG )
2021-08-27 11:52:48 -06:00
request_resource = RNS . Resource ( packed_request , self , request_id = request_id , is_response = False , timeout = timeout )
2021-08-21 06:52:31 -06:00
return RequestReceipt (
self ,
resource = request_resource ,
response_callback = response_callback ,
2021-08-27 11:52:48 -06:00
failed_callback = failed_callback ,
2021-08-28 12:01:01 -06:00
progress_callback = progress_callback ,
2022-05-24 12:13:11 -06:00
timeout = timeout ,
request_size = len ( packed_request ) ,
2021-08-21 06:52:31 -06:00
)
2021-08-20 15:29:06 -06:00
2020-08-13 04:15:56 -06:00
def rtt_packet ( self , packet ) :
try :
measured_rtt = time . time ( ) - self . request_time
plaintext = self . decrypt ( packet . data )
2023-10-31 04:44:12 -06:00
if plaintext != None :
rtt = umsgpack . unpackb ( plaintext )
self . rtt = max ( measured_rtt , rtt )
self . status = Link . ACTIVE
self . activated_at = time . time ( )
2021-12-11 09:26:45 -07:00
2023-10-31 04:44:12 -06:00
if self . rtt != None and self . establishment_cost != None and self . rtt > 0 and self . establishment_cost > 0 :
self . establishment_rate = self . establishment_cost / self . rtt
2023-02-17 01:54:18 -07:00
2023-10-31 04:44:12 -06:00
try :
if self . owner . callbacks . link_established != None :
self . owner . callbacks . link_established ( self )
except Exception as e :
RNS . log ( " Error occurred in external link establishment callback. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2023-02-17 01:54:18 -07:00
2020-08-13 04:15:56 -06:00
except Exception as e :
2021-05-20 12:32:08 -06:00
RNS . log ( " Error occurred while processing RTT packet, tearing down link. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2020-08-13 04:15:56 -06:00
self . teardown ( )
2023-10-27 16:05:35 -06:00
def track_phy_stats ( self , track ) :
"""
You can enable physical layer statistics on a per - link basis . If this is enabled ,
and the link is running over an interface that supports reporting physical layer
statistics , you will be able to retrieve stats such as * RSSI * , * SNR * and physical
* Link Quality * for the link .
: param track : Whether or not to keep track of physical layer statistics . Value must be ` ` True ` ` or ` ` False ` ` .
"""
if track :
self . __track_phy_stats = True
else :
self . __track_phy_stats = False
def get_rssi ( self ) :
"""
: returns : The physical layer * Received Signal Strength Indication * if available , otherwise ` ` None ` ` . Physical layer statistics must be enabled on the link for this method to return a value .
"""
2024-10-20 05:34:02 -06:00
if self . __track_phy_stats :
return self . rssi
else :
return None
2023-10-27 16:05:35 -06:00
def get_snr ( self ) :
"""
: returns : The physical layer * Signal - to - Noise Ratio * if available , otherwise ` ` None ` ` . Physical layer statistics must be enabled on the link for this method to return a value .
"""
2024-10-20 05:34:02 -06:00
if self . __track_phy_stats :
return self . snr
else :
return None
2023-10-27 16:05:35 -06:00
def get_q ( self ) :
"""
: returns : The physical layer * Link Quality * if available , otherwise ` ` None ` ` . Physical layer statistics must be enabled on the link for this method to return a value .
"""
2024-10-20 05:34:02 -06:00
if self . __track_phy_stats :
return self . q
else :
return None
2023-10-27 16:05:35 -06:00
2023-02-17 01:54:18 -07:00
def get_establishment_rate ( self ) :
"""
: returns : The data transfer rate at which the link establishment procedure ocurred , in bits per second .
"""
if self . establishment_rate != None :
return self . establishment_rate * 8
else :
return None
2021-05-16 08:37:12 -06:00
def get_salt ( self ) :
2020-08-13 04:15:56 -06:00
return self . link_id
2021-05-16 08:37:12 -06:00
def get_context ( self ) :
2020-08-13 04:15:56 -06:00
return None
2024-08-28 12:36:51 -06:00
def get_age ( self ) :
"""
: returns : The time in seconds since this link was established .
"""
if self . activated_at :
return time . time ( ) - self . activated_at
else :
return None
2021-05-14 04:58:02 -06:00
def no_inbound_for ( self ) :
2021-05-16 15:14:49 -06:00
"""
2024-02-29 15:05:16 -07:00
: returns : The time in seconds since last inbound packet on the link . This includes keepalive packets .
2021-05-16 15:14:49 -06:00
"""
2022-05-24 12:13:11 -06:00
activated_at = self . activated_at if self . activated_at != None else 0
last_inbound = max ( self . last_inbound , activated_at )
return time . time ( ) - last_inbound
2021-05-14 04:58:02 -06:00
def no_outbound_for ( self ) :
2021-05-16 15:14:49 -06:00
"""
2024-02-29 15:05:16 -07:00
: returns : The time in seconds since last outbound packet on the link . This includes keepalive packets .
2021-05-16 15:14:49 -06:00
"""
2021-05-14 04:58:02 -06:00
return time . time ( ) - self . last_outbound
2024-02-29 15:05:16 -07:00
def no_data_for ( self ) :
"""
: returns : The time in seconds since payload data traversed the link . This excludes keepalive packets .
"""
return time . time ( ) - self . last_data
2021-05-13 08:41:23 -06:00
def inactive_for ( self ) :
2021-05-16 15:14:49 -06:00
"""
2024-02-29 15:05:16 -07:00
: returns : The time in seconds since activity on the link . This includes keepalive packets .
2021-05-16 15:14:49 -06:00
"""
2021-05-14 04:58:02 -06:00
return min ( self . no_inbound_for ( ) , self . no_outbound_for ( ) )
2021-08-19 06:10:37 -06:00
def get_remote_identity ( self ) :
"""
2022-09-30 03:26:51 -06:00
: returns : The identity of the remote peer , if it is known . Calling this method will not query the remote initiator to reveal its identity . Returns ` ` None ` ` if the link initiator has not already independently called the ` ` identify ( identity ) ` ` method .
2021-08-19 06:10:37 -06:00
"""
return self . __remote_identity
2024-02-29 15:05:16 -07:00
def had_outbound ( self , is_keepalive = False ) :
2021-05-14 04:58:02 -06:00
self . last_outbound = time . time ( )
2024-02-29 15:05:16 -07:00
if not is_keepalive :
self . last_data = self . last_outbound
2021-05-13 08:41:23 -06:00
2020-08-13 04:15:56 -06:00
def teardown ( self ) :
2021-05-16 15:14:49 -06:00
"""
Closes the link and purges encryption keys . New keys will
be used if a new link to the same destination is established .
"""
2020-08-13 04:15:56 -06:00
if self . status != Link . PENDING and self . status != Link . CLOSED :
teardown_packet = RNS . Packet ( self , self . link_id , context = RNS . Packet . LINKCLOSE )
teardown_packet . send ( )
2021-05-14 04:58:02 -06:00
self . had_outbound ( )
2020-08-13 04:15:56 -06:00
self . status = Link . CLOSED
if self . initiator :
self . teardown_reason = Link . INITIATOR_CLOSED
else :
self . teardown_reason = Link . DESTINATION_CLOSED
self . link_closed ( )
def teardown_packet ( self , packet ) :
try :
plaintext = self . decrypt ( packet . data )
if plaintext == self . link_id :
self . status = Link . CLOSED
if self . initiator :
self . teardown_reason = Link . DESTINATION_CLOSED
else :
self . teardown_reason = Link . INITIATOR_CLOSED
2023-10-23 17:41:12 -06:00
self . __update_phy_stats ( packet )
2020-08-13 04:15:56 -06:00
self . link_closed ( )
except Exception as e :
pass
def link_closed ( self ) :
for resource in self . incoming_resources :
resource . cancel ( )
for resource in self . outgoing_resources :
resource . cancel ( )
2023-02-25 17:23:25 -07:00
if self . _channel :
2023-02-27 16:36:04 -07:00
self . _channel . _shutdown ( )
2020-08-13 04:15:56 -06:00
self . prv = None
self . pub = None
self . pub_bytes = None
self . shared_key = None
self . derived_key = None
2021-09-16 12:40:37 -06:00
if self . destination != None :
if self . destination . direction == RNS . Destination . IN :
if self in self . destination . links :
self . destination . links . remove ( self )
2020-08-13 04:15:56 -06:00
if self . callbacks . link_closed != None :
2021-10-15 06:36:50 -06:00
try :
self . callbacks . link_closed ( self )
except Exception as e :
RNS . log ( " Error while executing link closed callback from " + str ( self ) + " . The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2020-08-13 04:15:56 -06:00
def start_watchdog ( self ) :
thread = threading . Thread ( target = self . __watchdog_job )
2022-09-30 11:02:25 -06:00
thread . daemon = True
2020-08-13 04:15:56 -06:00
thread . start ( )
def __watchdog_job ( self ) :
while not self . status == Link . CLOSED :
while ( self . watchdog_lock ) :
2023-08-05 07:10:00 -06:00
rtt_wait = 0.025
if hasattr ( self , " rtt " ) and self . rtt :
rtt_wait = self . rtt
sleep ( max ( rtt_wait , 0.025 ) )
2020-08-13 04:15:56 -06:00
if not self . status == Link . CLOSED :
# Link was initiated, but no response
# from destination yet
if self . status == Link . PENDING :
2021-08-28 12:01:01 -06:00
next_check = self . request_time + self . establishment_timeout
2020-08-13 04:15:56 -06:00
sleep_time = next_check - time . time ( )
2021-08-28 12:01:01 -06:00
if time . time ( ) > = self . request_time + self . establishment_timeout :
2020-08-13 04:15:56 -06:00
RNS . log ( " Link establishment timed out " , RNS . LOG_VERBOSE )
self . status = Link . CLOSED
self . teardown_reason = Link . TIMEOUT
self . link_closed ( )
sleep_time = 0.001
elif self . status == Link . HANDSHAKE :
2021-08-28 12:01:01 -06:00
next_check = self . request_time + self . establishment_timeout
2020-08-13 04:15:56 -06:00
sleep_time = next_check - time . time ( )
2021-08-28 12:01:01 -06:00
if time . time ( ) > = self . request_time + self . establishment_timeout :
2020-08-13 04:15:56 -06:00
self . status = Link . CLOSED
self . teardown_reason = Link . TIMEOUT
self . link_closed ( )
sleep_time = 0.001
2023-06-13 08:10:47 -06:00
if self . initiator :
RNS . log ( " Timeout waiting for link request proof " , RNS . LOG_DEBUG )
else :
RNS . log ( " Timeout waiting for RTT packet from link initiator " , RNS . LOG_DEBUG )
2020-08-13 04:15:56 -06:00
elif self . status == Link . ACTIVE :
2022-05-24 12:13:11 -06:00
activated_at = self . activated_at if self . activated_at != None else 0
2023-05-11 08:22:44 -06:00
last_inbound = max ( max ( self . last_inbound , self . last_proof ) , activated_at )
2022-05-26 08:49:02 -06:00
2022-05-24 12:13:11 -06:00
if time . time ( ) > = last_inbound + self . keepalive :
2020-08-13 04:15:56 -06:00
if self . initiator :
self . send_keepalive ( )
2022-05-26 08:49:02 -06:00
if time . time ( ) > = last_inbound + self . stale_time :
sleep_time = self . rtt * self . keepalive_timeout_factor + Link . STALE_GRACE
self . status = Link . STALE
else :
sleep_time = self . keepalive
2020-08-13 04:15:56 -06:00
else :
2022-05-24 12:13:11 -06:00
sleep_time = ( last_inbound + self . keepalive ) - time . time ( )
2020-08-13 04:15:56 -06:00
elif self . status == Link . STALE :
sleep_time = 0.001
self . status = Link . CLOSED
self . teardown_reason = Link . TIMEOUT
self . link_closed ( )
if sleep_time == 0 :
RNS . log ( " Warning! Link watchdog sleep time of 0! " , RNS . LOG_ERROR )
if sleep_time == None or sleep_time < 0 :
RNS . log ( " Timing error! Tearing down link " + str ( self ) + " now. " , RNS . LOG_ERROR )
self . teardown ( )
2021-05-13 08:41:23 -06:00
sleep_time = 0.1
2020-08-13 04:15:56 -06:00
sleep ( sleep_time )
2024-10-20 05:34:02 -06:00
if not self . __track_phy_stats :
self . rssi = None
self . snr = None
self . q = None
2020-08-13 04:15:56 -06:00
2024-10-20 05:34:02 -06:00
def __update_phy_stats ( self , packet , query_shared = True , force_update = False ) :
if self . __track_phy_stats or force_update :
2023-10-27 16:05:35 -06:00
if query_shared :
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 )
if packet . rssi != None :
self . rssi = packet . rssi
if packet . snr != None :
self . snr = packet . snr
if packet . q != None :
self . q = packet . q
2023-10-23 17:41:12 -06:00
2020-08-13 04:15:56 -06:00
def send_keepalive ( self ) :
keepalive_packet = RNS . Packet ( self , bytes ( [ 0xFF ] ) , context = RNS . Packet . KEEPALIVE )
keepalive_packet . send ( )
2024-02-29 15:05:16 -07:00
self . had_outbound ( is_keepalive = True )
2020-08-13 04:15:56 -06:00
2021-08-21 06:52:31 -06:00
def handle_request ( self , request_id , unpacked_request ) :
2021-09-03 08:23:31 -06:00
if self . status == Link . ACTIVE :
requested_at = unpacked_request [ 0 ]
path_hash = unpacked_request [ 1 ]
request_data = unpacked_request [ 2 ]
if path_hash in self . destination . request_handlers :
request_handler = self . destination . request_handlers [ path_hash ]
path = request_handler [ 0 ]
response_generator = request_handler [ 1 ]
allow = request_handler [ 2 ]
allowed_list = request_handler [ 3 ]
allowed = False
if not allow == RNS . Destination . ALLOW_NONE :
if allow == RNS . Destination . ALLOW_LIST :
2022-05-24 12:13:11 -06:00
if self . __remote_identity != None and self . __remote_identity . hash in allowed_list :
2021-09-03 08:23:31 -06:00
allowed = True
elif allow == RNS . Destination . ALLOW_ALL :
2021-08-21 06:52:31 -06:00
allowed = True
2021-09-03 08:23:31 -06:00
if allowed :
RNS . log ( " Handling request " + RNS . prettyhexrep ( request_id ) + " for: " + str ( path ) , RNS . LOG_DEBUG )
2023-02-09 03:52:54 -07:00
if len ( inspect . signature ( response_generator ) . parameters ) == 5 :
response = response_generator ( path , request_data , request_id , self . __remote_identity , requested_at )
elif len ( inspect . signature ( response_generator ) . parameters ) == 6 :
response = response_generator ( path , request_data , request_id , self . link_id , self . __remote_identity , requested_at )
else :
raise TypeError ( " Invalid signature for response generator callback " )
2021-09-03 08:23:31 -06:00
if response != None :
packed_response = umsgpack . packb ( [ request_id , response ] )
2021-08-21 06:52:31 -06:00
2021-09-03 08:23:31 -06:00
if len ( packed_response ) < = Link . MDU :
RNS . Packet ( self , packed_response , RNS . Packet . DATA , context = RNS . Packet . RESPONSE ) . send ( )
else :
response_resource = RNS . Resource ( packed_response , self , request_id = request_id , is_response = True )
else :
2021-10-03 07:23:12 -06:00
identity_string = str ( self . get_remote_identity ( ) ) if self . get_remote_identity ( ) != None else " <Unknown> "
2021-09-03 08:23:31 -06:00
RNS . log ( " Request " + RNS . prettyhexrep ( request_id ) + " from " + identity_string + " not allowed for: " + str ( path ) , RNS . LOG_DEBUG )
2021-08-21 06:52:31 -06:00
2021-09-10 13:35:30 -06:00
def handle_response ( self , request_id , response_data , response_size , response_transfer_size ) :
2021-09-03 08:23:31 -06:00
if self . status == Link . ACTIVE :
remove = None
for pending_request in self . pending_requests :
if pending_request . request_id == request_id :
remove = pending_request
try :
2021-09-10 13:35:30 -06:00
pending_request . response_size = response_size
2024-01-13 16:46:55 -07:00
if pending_request . response_transfer_size == None :
pending_request . response_transfer_size = 0
pending_request . response_transfer_size + = response_transfer_size
2021-09-03 08:23:31 -06:00
pending_request . response_received ( response_data )
except Exception as e :
RNS . log ( " Error occurred while handling response. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
break
if remove != None :
2022-02-26 10:27:11 -07:00
if remove in self . pending_requests :
self . pending_requests . remove ( remove )
2021-08-21 06:52:31 -06:00
def request_resource_concluded ( self , resource ) :
if resource . status == RNS . Resource . COMPLETE :
packed_request = resource . data . read ( )
unpacked_request = umsgpack . unpackb ( packed_request )
request_id = RNS . Identity . truncated_hash ( packed_request )
request_data = unpacked_request
self . handle_request ( request_id , request_data )
else :
RNS . log ( " Incoming request resource failed with status: " + RNS . hexrep ( [ resource . status ] ) , RNS . LOG_DEBUG )
def response_resource_concluded ( self , resource ) :
if resource . status == RNS . Resource . COMPLETE :
packed_response = resource . data . read ( )
unpacked_response = umsgpack . unpackb ( packed_response )
request_id = unpacked_response [ 0 ]
response_data = unpacked_response [ 1 ]
2021-09-10 13:35:30 -06:00
self . handle_response ( request_id , response_data , resource . total_size , resource . size )
2021-08-21 06:52:31 -06:00
else :
RNS . log ( " Incoming response resource failed with status: " + RNS . hexrep ( [ resource . status ] ) , RNS . LOG_DEBUG )
2021-09-03 10:53:37 -06:00
for pending_request in self . pending_requests :
if pending_request . request_id == resource . request_id :
pending_request . request_timed_out ( None )
2021-08-21 06:52:31 -06:00
2023-02-26 06:25:49 -07:00
def get_channel ( self ) :
2023-02-27 18:05:25 -07:00
"""
Get the ` ` Channel ` ` for this link .
: return : ` ` Channel ` ` object
"""
2023-02-25 17:23:25 -07:00
if self . _channel is None :
self . _channel = Channel ( LinkChannelOutlet ( self ) )
return self . _channel
2020-08-13 04:15:56 -06:00
def receive ( self , packet ) :
self . watchdog_lock = True
if not self . status == Link . CLOSED and not ( self . initiator and packet . context == RNS . Packet . KEEPALIVE and packet . data == bytes ( [ 0xFF ] ) ) :
if packet . receiving_interface != self . attached_interface :
2024-09-21 11:04:28 -06:00
RNS . log ( f " Link-associated packet received on unexpected interface { packet . receiving_interface } instead of { self . attached_interface } ! Someone might be trying to manipulate your communication! " , RNS . LOG_ERROR )
2020-08-13 04:15:56 -06:00
else :
self . last_inbound = time . time ( )
2024-02-29 15:05:16 -07:00
if packet . context != RNS . Packet . KEEPALIVE :
self . last_data = self . last_inbound
2020-08-13 04:15:56 -06:00
self . rx + = 1
self . rxbytes + = len ( packet . data )
if self . status == Link . STALE :
self . status = Link . ACTIVE
if packet . packet_type == RNS . Packet . DATA :
2023-10-27 16:05:35 -06:00
should_query = False
2020-08-13 04:15:56 -06:00
if packet . context == RNS . Packet . NONE :
plaintext = self . decrypt ( packet . data )
2024-09-08 06:55:07 -06:00
packet . ratchet_id = self . link_id
2023-10-31 04:44:12 -06:00
if plaintext != None :
2024-10-20 05:34:02 -06:00
self . __update_phy_stats ( packet , query_shared = True )
2023-10-31 04:44:12 -06:00
if self . callbacks . packet != None :
thread = threading . Thread ( target = self . callbacks . packet , args = ( plaintext , packet ) )
thread . daemon = True
thread . start ( )
if self . destination . proof_strategy == RNS . Destination . PROVE_ALL :
packet . prove ( )
2020-08-13 04:15:56 -06:00
2023-10-31 04:44:12 -06:00
elif self . destination . proof_strategy == RNS . Destination . PROVE_APP :
if self . destination . callbacks . proof_requested :
try :
if self . destination . callbacks . proof_requested ( packet ) :
packet . prove ( )
except Exception as e :
RNS . log ( " Error while executing proof request callback from " + str ( self ) + " . The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2020-08-13 04:15:56 -06:00
2021-08-19 06:10:37 -06:00
elif packet . context == RNS . Packet . LINKIDENTIFY :
plaintext = self . decrypt ( packet . data )
2023-10-31 04:44:12 -06:00
if plaintext != None :
if not self . initiator and len ( plaintext ) == RNS . Identity . KEYSIZE / / 8 + RNS . Identity . SIGLENGTH / / 8 :
public_key = plaintext [ : RNS . Identity . KEYSIZE / / 8 ]
signed_data = self . link_id + public_key
signature = plaintext [ RNS . Identity . KEYSIZE / / 8 : RNS . Identity . KEYSIZE / / 8 + RNS . Identity . SIGLENGTH / / 8 ]
identity = RNS . Identity ( create_keys = False )
identity . load_public_key ( public_key )
if identity . validate ( signature , signed_data ) :
self . __remote_identity = identity
if self . callbacks . remote_identified != None :
try :
self . callbacks . remote_identified ( self , self . __remote_identity )
except Exception as e :
RNS . log ( " Error while executing remote identified callback from " + str ( self ) + " . The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
self . __update_phy_stats ( packet , query_shared = True )
2021-08-19 06:10:37 -06:00
2021-08-20 15:29:06 -06:00
elif packet . context == RNS . Packet . REQUEST :
try :
request_id = packet . getTruncatedHash ( )
packed_request = self . decrypt ( packet . data )
2023-10-31 04:44:12 -06:00
if packed_request != None :
unpacked_request = umsgpack . unpackb ( packed_request )
self . handle_request ( request_id , unpacked_request )
self . __update_phy_stats ( packet , query_shared = True )
2021-08-20 15:29:06 -06:00
except Exception as e :
RNS . log ( " Error occurred while handling request. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
elif packet . context == RNS . Packet . RESPONSE :
2021-08-21 06:52:31 -06:00
try :
packed_response = self . decrypt ( packet . data )
2023-10-31 04:44:12 -06:00
if packed_response != None :
unpacked_response = umsgpack . unpackb ( packed_response )
request_id = unpacked_response [ 0 ]
response_data = unpacked_response [ 1 ]
transfer_size = len ( umsgpack . packb ( response_data ) ) - 2
self . handle_response ( request_id , response_data , transfer_size , transfer_size )
self . __update_phy_stats ( packet , query_shared = True )
2021-08-21 06:52:31 -06:00
except Exception as e :
RNS . log ( " Error occurred while handling response. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2021-08-20 15:29:06 -06:00
2020-08-13 04:15:56 -06:00
elif packet . context == RNS . Packet . LRRTT :
if not self . initiator :
self . rtt_packet ( packet )
2023-10-27 16:05:35 -06:00
self . __update_phy_stats ( packet , query_shared = True )
2020-08-13 04:15:56 -06:00
elif packet . context == RNS . Packet . LINKCLOSE :
self . teardown_packet ( packet )
2023-10-27 16:05:35 -06:00
self . __update_phy_stats ( packet , query_shared = True )
2020-08-13 04:15:56 -06:00
elif packet . context == RNS . Packet . RESOURCE_ADV :
packet . plaintext = self . decrypt ( packet . data )
2023-10-31 04:44:12 -06:00
if packet . plaintext != None :
self . __update_phy_stats ( packet , query_shared = True )
2021-08-21 06:52:31 -06:00
2023-10-31 04:44:12 -06:00
if RNS . ResourceAdvertisement . is_request ( packet ) :
RNS . Resource . accept ( packet , callback = self . request_resource_concluded )
elif RNS . ResourceAdvertisement . is_response ( packet ) :
request_id = RNS . ResourceAdvertisement . read_request_id ( packet )
for pending_request in self . pending_requests :
if pending_request . request_id == request_id :
2023-11-04 10:59:27 -06:00
response_resource = RNS . Resource . accept ( packet , callback = self . response_resource_concluded , progress_callback = pending_request . response_resource_progress , request_id = request_id )
2024-01-13 08:36:11 -07:00
if response_resource != None :
2024-01-13 16:46:55 -07:00
if pending_request . response_size == None :
pending_request . response_size = RNS . ResourceAdvertisement . read_size ( packet )
if pending_request . response_transfer_size == None :
pending_request . response_transfer_size = 0
pending_request . response_transfer_size + = RNS . ResourceAdvertisement . read_transfer_size ( packet )
if pending_request . started_at == None :
pending_request . started_at = time . time ( )
2024-01-13 08:36:11 -07:00
pending_request . response_resource_progress ( response_resource )
2023-11-04 10:59:27 -06:00
2023-10-31 04:44:12 -06:00
elif self . resource_strategy == Link . ACCEPT_NONE :
pass
elif self . resource_strategy == Link . ACCEPT_APP :
if self . callbacks . resource != None :
try :
resource_advertisement = RNS . ResourceAdvertisement . unpack ( packet . plaintext )
resource_advertisement . link = self
if self . callbacks . resource ( resource_advertisement ) :
RNS . Resource . accept ( packet , self . callbacks . resource_concluded )
except Exception as e :
RNS . log ( " Error while executing resource accept callback from " + str ( self ) + " . The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
elif self . resource_strategy == Link . ACCEPT_ALL :
RNS . Resource . accept ( packet , self . callbacks . resource_concluded )
2020-08-13 04:15:56 -06:00
elif packet . context == RNS . Packet . RESOURCE_REQ :
plaintext = self . decrypt ( packet . data )
2023-10-31 04:44:12 -06:00
if plaintext != None :
self . __update_phy_stats ( packet , query_shared = True )
if ord ( plaintext [ : 1 ] ) == RNS . Resource . HASHMAP_IS_EXHAUSTED :
resource_hash = plaintext [ 1 + RNS . Resource . MAPHASH_LEN : RNS . Identity . HASHLENGTH / / 8 + 1 + RNS . Resource . MAPHASH_LEN ]
else :
resource_hash = plaintext [ 1 : RNS . Identity . HASHLENGTH / / 8 + 1 ]
for resource in self . outgoing_resources :
if resource . hash == resource_hash :
# We need to check that this request has not been
# received before in order to avoid sequencing errors.
if not packet . packet_hash in resource . req_hashlist :
resource . req_hashlist . append ( packet . packet_hash )
resource . request ( plaintext )
2024-08-30 05:33:51 -06:00
# TODO: Test and possibly enable this at some point
# def request_job():
# resource.request(plaintext)
# threading.Thread(target=request_job, daemon=True).start()
2020-08-13 04:15:56 -06:00
elif packet . context == RNS . Packet . RESOURCE_HMU :
plaintext = self . decrypt ( packet . data )
2023-10-31 04:44:12 -06:00
if plaintext != None :
self . __update_phy_stats ( packet , query_shared = True )
resource_hash = plaintext [ : RNS . Identity . HASHLENGTH / / 8 ]
for resource in self . incoming_resources :
if resource_hash == resource . hash :
resource . hashmap_update_packet ( plaintext )
2020-08-13 04:15:56 -06:00
elif packet . context == RNS . Packet . RESOURCE_ICL :
plaintext = self . decrypt ( packet . data )
2023-10-31 04:44:12 -06:00
if plaintext != None :
self . __update_phy_stats ( packet )
resource_hash = plaintext [ : RNS . Identity . HASHLENGTH / / 8 ]
for resource in self . incoming_resources :
if resource_hash == resource . hash :
resource . cancel ( )
2020-08-13 04:15:56 -06:00
elif packet . context == RNS . Packet . KEEPALIVE :
if not self . initiator and packet . data == bytes ( [ 0xFF ] ) :
keepalive_packet = RNS . Packet ( self , bytes ( [ 0xFE ] ) , context = RNS . Packet . KEEPALIVE )
keepalive_packet . send ( )
2024-02-29 15:05:16 -07:00
self . had_outbound ( is_keepalive = True )
2020-08-13 04:15:56 -06:00
# TODO: find the most efficient way to allow multiple
# transfers at the same time, sending resource hash on
# each packet is a huge overhead. Probably some kind
# of hash -> sequence map
elif packet . context == RNS . Packet . RESOURCE :
for resource in self . incoming_resources :
resource . receive_part ( packet )
2023-10-23 17:41:12 -06:00
self . __update_phy_stats ( packet )
2020-08-13 04:15:56 -06:00
2023-02-25 17:23:25 -07:00
elif packet . context == RNS . Packet . CHANNEL :
if not self . _channel :
RNS . log ( f " Channel data received without open channel " , RNS . LOG_DEBUG )
else :
2023-03-04 22:37:58 -07:00
packet . prove ( )
2023-02-25 17:23:25 -07:00
plaintext = self . decrypt ( packet . data )
2023-10-31 04:44:12 -06:00
if plaintext != None :
self . __update_phy_stats ( packet )
self . _channel . _receive ( plaintext )
2023-02-25 17:23:25 -07:00
2020-08-13 04:15:56 -06:00
elif packet . packet_type == RNS . Packet . PROOF :
if packet . context == RNS . Packet . RESOURCE_PRF :
resource_hash = packet . data [ 0 : RNS . Identity . HASHLENGTH / / 8 ]
for resource in self . outgoing_resources :
if resource_hash == resource . hash :
2021-05-16 08:42:07 -06:00
resource . validate_proof ( packet . data )
2023-10-27 16:05:35 -06:00
self . __update_phy_stats ( packet , query_shared = True )
2020-08-13 04:15:56 -06:00
self . watchdog_lock = False
def encrypt ( self , plaintext ) :
try :
2024-11-22 07:19:12 -07:00
if not self . token :
2021-09-03 06:42:59 -06:00
try :
2024-11-22 07:19:12 -07:00
self . token = Token ( self . derived_key )
2021-09-03 06:42:59 -06:00
except Exception as e :
2024-11-22 07:19:12 -07:00
RNS . log ( " Could not instantiate token while performing encryption on link " + str ( self ) + " . The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2021-09-03 06:42:59 -06:00
raise e
2021-05-17 06:05:13 -06:00
2024-11-22 07:19:12 -07:00
return self . token . encrypt ( plaintext )
2021-09-02 10:34:58 -06:00
2020-08-13 04:15:56 -06:00
except Exception as e :
RNS . log ( " Encryption on link " + str ( self ) + " failed. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2021-08-27 14:51:16 -06:00
raise e
2020-08-13 04:15:56 -06:00
def decrypt ( self , ciphertext ) :
try :
2024-11-22 07:19:12 -07:00
if not self . token :
self . token = Token ( self . derived_key )
2021-05-17 06:05:13 -06:00
2024-11-22 07:19:12 -07:00
return self . token . decrypt ( ciphertext )
2022-06-30 06:02:57 -06:00
2020-08-13 04:15:56 -06:00
except Exception as e :
RNS . log ( " Decryption failed on link " + str ( self ) + " . The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2023-10-31 04:44:12 -06:00
return None
2021-05-13 08:41:23 -06:00
2020-08-13 04:15:56 -06:00
def sign ( self , message ) :
2021-05-17 06:05:13 -06:00
return self . sig_prv . sign ( message )
2020-08-13 04:15:56 -06:00
def validate ( self , signature , message ) :
try :
2021-05-17 06:05:13 -06:00
self . peer_sig_pub . verify ( signature , message )
2020-08-13 04:15:56 -06:00
return True
except Exception as e :
return False
2021-05-20 07:31:38 -06:00
def set_link_established_callback ( self , callback ) :
2020-08-13 04:15:56 -06:00
self . callbacks . link_established = callback
2021-05-20 07:31:38 -06:00
def set_link_closed_callback ( self , callback ) :
2022-05-24 12:13:11 -06:00
"""
Registers a function to be called when a link has been
torn down .
: param callback : A function or method with the signature * callback ( link ) * to be called .
"""
2020-08-13 04:15:56 -06:00
self . callbacks . link_closed = callback
2021-05-20 07:31:38 -06:00
def set_packet_callback ( self , callback ) :
2021-05-16 15:14:49 -06:00
"""
Registers a function to be called when a packet has been
received over this link .
: param callback : A function or method with the signature * callback ( message , packet ) * to be called .
"""
2020-08-13 04:15:56 -06:00
self . callbacks . packet = callback
2021-05-20 07:31:38 -06:00
def set_resource_callback ( self , callback ) :
2021-05-16 15:14:49 -06:00
"""
Registers a function to be called when a resource has been
advertised over this link . If the function returns * True *
the resource will be accepted . If it returns * False * it will
be ignored .
2022-05-22 11:08:03 -06:00
: param callback : A function or method with the signature * callback ( resource ) * to be called . Please note that only the basic information of the resource is available at this time , such as * get_transfer_size ( ) * , * get_data_size ( ) * , * get_parts ( ) * and * is_compressed ( ) * .
2021-05-16 15:14:49 -06:00
"""
self . callbacks . resource = callback
2021-05-20 07:31:38 -06:00
def set_resource_started_callback ( self , callback ) :
2021-05-16 15:14:49 -06:00
"""
Registers a function to be called when a resource has begun
transferring over this link .
: param callback : A function or method with the signature * callback ( resource ) * to be called .
"""
2020-08-13 04:15:56 -06:00
self . callbacks . resource_started = callback
2021-05-20 07:31:38 -06:00
def set_resource_concluded_callback ( self , callback ) :
2021-05-16 15:14:49 -06:00
"""
Registers a function to be called when a resource has concluded
transferring over this link .
: param callback : A function or method with the signature * callback ( resource ) * to be called .
"""
2020-08-13 04:15:56 -06:00
self . callbacks . resource_concluded = callback
2021-08-19 06:10:37 -06:00
def set_remote_identified_callback ( self , callback ) :
"""
Registers a function to be called when an initiating peer has
identified over this link .
2022-05-22 11:08:03 -06:00
: param callback : A function or method with the signature * callback ( link , identity ) * to be called .
2021-08-19 06:10:37 -06:00
"""
self . callbacks . remote_identified = callback
2020-08-13 04:15:56 -06:00
def resource_concluded ( self , resource ) :
if resource in self . incoming_resources :
self . incoming_resources . remove ( resource )
if resource in self . outgoing_resources :
self . outgoing_resources . remove ( resource )
def set_resource_strategy ( self , resource_strategy ) :
2021-05-16 15:14:49 -06:00
"""
Sets the resource strategy for the link .
: param resource_strategy : One of ` ` RNS . Link . ACCEPT_NONE ` ` , ` ` RNS . Link . ACCEPT_ALL ` ` or ` ` RNS . Link . ACCEPT_APP ` ` . If ` ` RNS . Link . ACCEPT_APP ` ` is set , the ` resource_callback ` will be called to determine whether the resource should be accepted or not .
: raises : * TypeError * if the resource strategy is unsupported .
"""
2020-08-13 04:15:56 -06:00
if not resource_strategy in Link . resource_strategies :
raise TypeError ( " Unsupported resource strategy " )
else :
self . resource_strategy = resource_strategy
def register_outgoing_resource ( self , resource ) :
self . outgoing_resources . append ( resource )
def register_incoming_resource ( self , resource ) :
self . incoming_resources . append ( resource )
2022-12-18 17:04:49 -07:00
def has_incoming_resource ( self , resource ) :
for incoming_resource in self . incoming_resources :
if incoming_resource . hash == resource . hash :
return True
return False
2020-08-13 04:15:56 -06:00
def cancel_outgoing_resource ( self , resource ) :
if resource in self . outgoing_resources :
self . outgoing_resources . remove ( resource )
else :
RNS . log ( " Attempt to cancel a non-existing outgoing resource " , RNS . LOG_ERROR )
def cancel_incoming_resource ( self , resource ) :
if resource in self . incoming_resources :
self . incoming_resources . remove ( resource )
else :
RNS . log ( " Attempt to cancel a non-existing incoming resource " , RNS . LOG_ERROR )
def ready_for_new_resource ( self ) :
if len ( self . outgoing_resources ) > 0 :
return False
else :
return True
def __str__ ( self ) :
2021-08-20 15:29:06 -06:00
return RNS . prettyhexrep ( self . link_id )
class RequestReceipt ( ) :
2021-09-02 12:35:42 -06:00
"""
An instance of this class is returned by the ` ` request ` ` method of ` ` RNS . Link ` `
instances . It should never be instantiated manually . It provides methods to
check status , response time and response data when the request concludes .
"""
2021-08-20 15:29:06 -06:00
FAILED = 0x00
SENT = 0x01
DELIVERED = 0x02
2021-09-03 13:08:20 -06:00
RECEIVING = 0x03
READY = 0x04
2021-08-20 15:29:06 -06:00
2022-05-24 12:13:11 -06:00
def __init__ ( self , link , packet_receipt = None , resource = None , response_callback = None , failed_callback = None , progress_callback = None , timeout = None , request_size = None ) :
2021-08-21 06:52:31 -06:00
self . packet_receipt = packet_receipt
self . resource = resource
2021-08-28 12:01:01 -06:00
self . started_at = None
2021-08-21 06:52:31 -06:00
if self . packet_receipt != None :
self . hash = packet_receipt . truncated_hash
self . packet_receipt . set_timeout_callback ( self . request_timed_out )
2021-08-28 12:01:01 -06:00
self . started_at = time . time ( )
2021-08-21 06:52:31 -06:00
elif self . resource != None :
self . hash = resource . request_id
resource . set_callback ( self . request_resource_concluded )
2021-08-28 12:01:01 -06:00
self . link = link
self . request_id = self . hash
2022-05-24 12:13:11 -06:00
self . request_size = request_size
2021-08-28 12:01:01 -06:00
self . response = None
self . response_transfer_size = None
self . response_size = None
self . status = RequestReceipt . SENT
self . sent_at = time . time ( )
self . progress = 0
self . concluded_at = None
self . response_concluded_at = None
2021-08-20 15:29:06 -06:00
2021-08-27 11:52:48 -06:00
if timeout != None :
self . timeout = timeout
else :
2021-08-28 12:01:01 -06:00
raise ValueError ( " No timeout specified for request receipt " )
2021-08-27 11:52:48 -06:00
2021-08-20 15:29:06 -06:00
self . callbacks = RequestReceiptCallbacks ( )
self . callbacks . response = response_callback
self . callbacks . failed = failed_callback
2021-08-28 12:01:01 -06:00
self . callbacks . progress = progress_callback
2021-08-20 15:29:06 -06:00
self . link . pending_requests . append ( self )
2021-08-27 11:52:48 -06:00
2021-08-21 06:52:31 -06:00
def request_resource_concluded ( self , resource ) :
if resource . status == RNS . Resource . COMPLETE :
RNS . log ( " Request " + RNS . prettyhexrep ( self . request_id ) + " successfully sent as resource. " , RNS . LOG_DEBUG )
2024-01-13 16:46:55 -07:00
if self . started_at == None :
self . started_at = time . time ( )
2021-08-27 11:52:48 -06:00
self . status = RequestReceipt . DELIVERED
self . __resource_response_timeout = time . time ( ) + self . timeout
2021-09-03 13:08:20 -06:00
response_timeout_thread = threading . Thread ( target = self . __response_timeout_job )
2022-09-30 11:02:25 -06:00
response_timeout_thread . daemon = True
2021-09-03 13:08:20 -06:00
response_timeout_thread . start ( )
2021-08-21 06:52:31 -06:00
else :
RNS . log ( " Sending request " + RNS . prettyhexrep ( self . request_id ) + " as resource failed with status: " + RNS . hexrep ( [ resource . status ] ) , RNS . LOG_DEBUG )
self . status = RequestReceipt . FAILED
self . concluded_at = time . time ( )
self . link . pending_requests . remove ( self )
if self . callbacks . failed != None :
2021-10-15 06:36:50 -06:00
try :
self . callbacks . failed ( self )
except Exception as e :
RNS . log ( " Error while executing request failed callback from " + str ( self ) + " . The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2021-08-20 15:29:06 -06:00
2021-08-27 11:52:48 -06:00
2021-09-03 13:08:20 -06:00
def __response_timeout_job ( self ) :
while self . status == RequestReceipt . DELIVERED :
now = time . time ( )
if now > self . __resource_response_timeout :
self . request_timed_out ( None )
time . sleep ( 0.1 )
2021-08-20 15:29:06 -06:00
def request_timed_out ( self , packet_receipt ) :
self . status = RequestReceipt . FAILED
self . concluded_at = time . time ( )
self . link . pending_requests . remove ( self )
if self . callbacks . failed != None :
2021-10-15 06:36:50 -06:00
try :
self . callbacks . failed ( self )
except Exception as e :
RNS . log ( " Error while executing request timed out callback from " + str ( self ) + " . The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2021-08-20 15:29:06 -06:00
2021-09-03 13:08:20 -06:00
2021-08-28 12:01:01 -06:00
def response_resource_progress ( self , resource ) :
2024-01-13 08:36:11 -07:00
if resource != None :
if not self . status == RequestReceipt . FAILED :
self . status = RequestReceipt . RECEIVING
if self . packet_receipt != None :
if self . packet_receipt . status != RNS . PacketReceipt . DELIVERED :
self . packet_receipt . status = RNS . PacketReceipt . DELIVERED
self . packet_receipt . proved = True
self . packet_receipt . concluded_at = time . time ( )
if self . packet_receipt . callbacks . delivery != None :
self . packet_receipt . callbacks . delivery ( self . packet_receipt )
self . progress = resource . get_progress ( )
if self . callbacks . progress != None :
try :
self . callbacks . progress ( self )
except Exception as e :
RNS . log ( " Error while executing response progress callback from " + str ( self ) + " . The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
else :
resource . cancel ( )
2021-08-28 12:01:01 -06:00
2021-08-27 11:52:48 -06:00
2021-08-20 15:29:06 -06:00
def response_received ( self , response ) :
2021-09-03 06:22:53 -06:00
if not self . status == RequestReceipt . FAILED :
self . progress = 1.0
self . response = response
self . status = RequestReceipt . READY
self . response_concluded_at = time . time ( )
if self . packet_receipt != None :
self . packet_receipt . status = RNS . PacketReceipt . DELIVERED
self . packet_receipt . proved = True
self . packet_receipt . concluded_at = time . time ( )
if self . packet_receipt . callbacks . delivery != None :
self . packet_receipt . callbacks . delivery ( self . packet_receipt )
if self . callbacks . progress != None :
2021-10-15 06:36:50 -06:00
try :
self . callbacks . progress ( self )
except Exception as e :
RNS . log ( " Error while executing response progress callback from " + str ( self ) + " . The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2021-09-03 06:22:53 -06:00
if self . callbacks . response != None :
2021-10-15 06:36:50 -06:00
try :
self . callbacks . response ( self )
except Exception as e :
RNS . log ( " Error while executing response received callback from " + str ( self ) + " . The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2021-08-20 15:29:06 -06:00
2021-09-02 12:35:42 -06:00
def get_request_id ( self ) :
"""
: returns : The request ID as * bytes * .
"""
return self . request_id
def get_status ( self ) :
"""
: returns : The current status of the request , one of ` ` RNS . RequestReceipt . FAILED ` ` , ` ` RNS . RequestReceipt . SENT ` ` , ` ` RNS . RequestReceipt . DELIVERED ` ` , ` ` RNS . RequestReceipt . READY ` ` .
"""
return self . status
def get_progress ( self ) :
"""
: returns : The progress of a response being received as a * float * between 0.0 and 1.0 .
"""
return self . progress
def get_response ( self ) :
"""
: returns : The response as * bytes * if it is ready , otherwise * None * .
"""
if self . status == RequestReceipt . READY :
return self . response
else :
return None
def get_response_time ( self ) :
"""
: returns : The response time of the request in seconds .
"""
2021-08-28 12:01:01 -06:00
if self . status == RequestReceipt . READY :
return self . response_concluded_at - self . started_at
2021-09-02 12:35:42 -06:00
else :
return None
2021-08-28 12:01:01 -06:00
2024-08-29 05:14:55 -06:00
def concluded ( self ) :
"""
: returns : True if the associated request has concluded ( successfully or with a failure ) , otherwise False .
"""
if self . status == RequestReceipt . READY :
return True
elif self . status == RequestReceipt . FAILED :
return True
else :
return False
2021-08-28 12:01:01 -06:00
2021-08-20 15:29:06 -06:00
class RequestReceiptCallbacks :
def __init__ ( self ) :
self . response = None
2021-08-28 12:01:01 -06:00
self . failed = None
2022-03-07 16:38:51 -07:00
self . progress = None