mirror of
https://github.com/markqvist/Reticulum.git
synced 2024-11-22 21:50:18 +00:00
Implemented elliptic curve signatures and verify on links, auto proofs on links
This commit is contained in:
parent
dc86f6884a
commit
d69f3c2c34
@ -138,7 +138,7 @@ def client(destination_hexhash, configpath, timeout=None):
|
|||||||
# a callback function, that will get called if
|
# a callback function, that will get called if
|
||||||
# the packet times out.
|
# the packet times out.
|
||||||
if timeout != None:
|
if timeout != None:
|
||||||
packet_receipt.setTimeout(timeout)
|
packet_receipt.set_timeout(timeout)
|
||||||
packet_receipt.timeout_callback(packet_timed_out)
|
packet_receipt.timeout_callback(packet_timed_out)
|
||||||
|
|
||||||
# We can then set a delivery callback on the receipt.
|
# We can then set a delivery callback on the receipt.
|
||||||
|
@ -56,10 +56,13 @@ class Identity:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def loadKnownDestinations():
|
def loadKnownDestinations():
|
||||||
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
|
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
|
||||||
file = open(RNS.Reticulum.storagepath+"/known_destinations","r")
|
try:
|
||||||
Identity.known_destinations = umsgpack.load(file)
|
file = open(RNS.Reticulum.storagepath+"/known_destinations","r")
|
||||||
file.close()
|
Identity.known_destinations = umsgpack.load(file)
|
||||||
RNS.log("Loaded "+str(len(Identity.known_destinations))+" known destinations from storage", RNS.LOG_VERBOSE)
|
file.close()
|
||||||
|
RNS.log("Loaded "+str(len(Identity.known_destinations))+" known destinations from storage", RNS.LOG_VERBOSE)
|
||||||
|
except:
|
||||||
|
RNS.log("Error loading known destinations from disk, file will be recreated on exit", RNS.LOG_ERROR)
|
||||||
else:
|
else:
|
||||||
RNS.log("Destinations file does not exist, so no known destinations loaded", RNS.LOG_VERBOSE)
|
RNS.log("Destinations file does not exist, so no known destinations loaded", RNS.LOG_VERBOSE)
|
||||||
|
|
||||||
|
@ -2,19 +2,16 @@ from Interface import Interface
|
|||||||
import SocketServer
|
import SocketServer
|
||||||
import threading
|
import threading
|
||||||
import socket
|
import socket
|
||||||
|
import time
|
||||||
import sys
|
import sys
|
||||||
import RNS
|
import RNS
|
||||||
|
|
||||||
class UdpInterface(Interface):
|
class UdpInterface(Interface):
|
||||||
bind_ip = None
|
|
||||||
bind_port = None
|
|
||||||
forward_ip = None
|
|
||||||
forward_port = None
|
|
||||||
owner = None
|
|
||||||
|
|
||||||
def __init__(self, owner, name, bindip=None, bindport=None, forwardip=None, forwardport=None):
|
def __init__(self, owner, name, bindip=None, bindport=None, forwardip=None, forwardport=None):
|
||||||
self.IN = True
|
self.IN = True
|
||||||
self.OUT = False
|
self.OUT = False
|
||||||
|
self.transmit_delay = 0.001
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
@ -42,6 +39,7 @@ class UdpInterface(Interface):
|
|||||||
self.owner.inbound(data, self)
|
self.owner.inbound(data, self)
|
||||||
|
|
||||||
def processOutgoing(self,data):
|
def processOutgoing(self,data):
|
||||||
|
time.sleep(self.transmit_delay)
|
||||||
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||||
udp_socket.sendto(data, (self.forward_ip, self.forward_port))
|
udp_socket.sendto(data, (self.forward_ip, self.forward_port))
|
||||||
|
45
RNS/Link.py
45
RNS/Link.py
@ -30,7 +30,7 @@ class Link:
|
|||||||
# but calculated from something like
|
# but calculated from something like
|
||||||
# first-hop RTT latency and distance
|
# first-hop RTT latency and distance
|
||||||
DEFAULT_TIMEOUT = 5
|
DEFAULT_TIMEOUT = 5
|
||||||
TIMEOUT_FACTOR = 3
|
TIMEOUT_FACTOR = 5
|
||||||
KEEPALIVE = 120
|
KEEPALIVE = 120
|
||||||
|
|
||||||
PENDING = 0x00
|
PENDING = 0x00
|
||||||
@ -54,6 +54,7 @@ class Link:
|
|||||||
try:
|
try:
|
||||||
link = Link(owner = owner, peer_pub_bytes = data[:Link.ECPUBSIZE])
|
link = Link(owner = owner, peer_pub_bytes = data[:Link.ECPUBSIZE])
|
||||||
link.setLinkID(packet)
|
link.setLinkID(packet)
|
||||||
|
link.destination = packet.destination
|
||||||
RNS.log("Validating link request "+RNS.prettyhexrep(link.link_id), RNS.LOG_VERBOSE)
|
RNS.log("Validating link request "+RNS.prettyhexrep(link.link_id), RNS.LOG_VERBOSE)
|
||||||
link.handshake()
|
link.handshake()
|
||||||
link.attached_interface = packet.receiving_interface
|
link.attached_interface = packet.receiving_interface
|
||||||
@ -137,7 +138,8 @@ class Link:
|
|||||||
def loadPeer(self, peer_pub_bytes):
|
def loadPeer(self, peer_pub_bytes):
|
||||||
self.peer_pub_bytes = peer_pub_bytes
|
self.peer_pub_bytes = peer_pub_bytes
|
||||||
self.peer_pub = serialization.load_der_public_key(peer_pub_bytes, backend=default_backend())
|
self.peer_pub = serialization.load_der_public_key(peer_pub_bytes, backend=default_backend())
|
||||||
self.peer_pub.curce = Link.CURVE
|
if not hasattr(self.peer_pub, "curve"):
|
||||||
|
self.peer_pub.curve = Link.CURVE
|
||||||
|
|
||||||
def setLinkID(self, packet):
|
def setLinkID(self, packet):
|
||||||
self.link_id = RNS.Identity.truncatedHash(packet.raw)
|
self.link_id = RNS.Identity.truncatedHash(packet.raw)
|
||||||
@ -162,6 +164,18 @@ class Link:
|
|||||||
proof = RNS.Packet(self, proof_data, packet_type=RNS.Packet.PROOF, context=RNS.Packet.LRPROOF)
|
proof = RNS.Packet(self, proof_data, packet_type=RNS.Packet.PROOF, context=RNS.Packet.LRPROOF)
|
||||||
proof.send()
|
proof.send()
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
def validateProof(self, packet):
|
def validateProof(self, packet):
|
||||||
if self.initiator:
|
if self.initiator:
|
||||||
peer_pub_bytes = packet.data[:Link.ECPUBSIZE]
|
peer_pub_bytes = packet.data[:Link.ECPUBSIZE]
|
||||||
@ -326,9 +340,16 @@ class Link:
|
|||||||
if packet.packet_type == RNS.Packet.DATA:
|
if packet.packet_type == RNS.Packet.DATA:
|
||||||
if packet.context == RNS.Packet.NONE:
|
if packet.context == RNS.Packet.NONE:
|
||||||
plaintext = self.decrypt(packet.data)
|
plaintext = self.decrypt(packet.data)
|
||||||
if (self.callbacks.packet != None):
|
if self.callbacks.packet != None:
|
||||||
self.callbacks.packet(plaintext, packet)
|
self.callbacks.packet(plaintext, packet)
|
||||||
|
|
||||||
|
if self.destination.proof_strategy == RNS.Destination.PROVE_ALL:
|
||||||
|
packet.prove()
|
||||||
|
|
||||||
|
elif self.destination.proof_strategy == RNS.Destination.PROVE_APP:
|
||||||
|
if self.destination.callbacks.proof_requested:
|
||||||
|
self.destination.callbacks.proof_requested(packet)
|
||||||
|
|
||||||
elif packet.context == RNS.Packet.LRRTT:
|
elif packet.context == RNS.Packet.LRRTT:
|
||||||
if not self.initiator:
|
if not self.initiator:
|
||||||
self.rtt_packet(packet)
|
self.rtt_packet(packet)
|
||||||
@ -415,6 +436,16 @@ class Link:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
RNS.log("Decryption failed on link "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
RNS.log("Decryption failed on link "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||||
|
|
||||||
|
def sign(self, message):
|
||||||
|
return self.prv.sign(message, ec.ECDSA(hashes.SHA256()))
|
||||||
|
|
||||||
|
def validate(self, signature, message):
|
||||||
|
try:
|
||||||
|
self.peer_pub.verify(signature, message, ec.ECDSA(hashes.SHA256()))
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
return False
|
||||||
|
|
||||||
def link_established_callback(self, callback):
|
def link_established_callback(self, callback):
|
||||||
self.callbacks.link_established = callback
|
self.callbacks.link_established = callback
|
||||||
|
|
||||||
@ -432,7 +463,13 @@ class Link:
|
|||||||
def resource_concluded_callback(self, callback):
|
def resource_concluded_callback(self, callback):
|
||||||
self.callbacks.resource_concluded = callback
|
self.callbacks.resource_concluded = callback
|
||||||
|
|
||||||
def setResourceStrategy(self, resource_strategy):
|
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):
|
||||||
if not resource_strategy in Link.resource_strategies:
|
if not resource_strategy in Link.resource_strategies:
|
||||||
raise TypeError("Unsupported resource strategy")
|
raise TypeError("Unsupported resource strategy")
|
||||||
else:
|
else:
|
||||||
|
@ -17,7 +17,7 @@ class Packet:
|
|||||||
HEADER_4 = 0x03 # Reserved
|
HEADER_4 = 0x03 # Reserved
|
||||||
header_types = [HEADER_1, HEADER_2, HEADER_3, HEADER_4]
|
header_types = [HEADER_1, HEADER_2, HEADER_3, HEADER_4]
|
||||||
|
|
||||||
# Context types
|
# Data packet context types
|
||||||
NONE = 0x00 # Generic data packet
|
NONE = 0x00 # Generic data packet
|
||||||
RESOURCE = 0x01 # Packet is part of a resource
|
RESOURCE = 0x01 # Packet is part of a resource
|
||||||
RESOURCE_ADV = 0x02 # Packet is a resource advertisement
|
RESOURCE_ADV = 0x02 # Packet is a resource advertisement
|
||||||
@ -31,8 +31,9 @@ class Packet:
|
|||||||
RESPONSE = 0x0A # Packet is a response to a request
|
RESPONSE = 0x0A # Packet is a response to a request
|
||||||
COMMAND = 0x0B # Packet is a command
|
COMMAND = 0x0B # Packet is a command
|
||||||
COMMAND_STATUS = 0x0C # Packet is a status of an executed command
|
COMMAND_STATUS = 0x0C # Packet is a status of an executed command
|
||||||
KEEPALIVE = 0xFC # Packet is a keepalive packet
|
KEEPALIVE = 0xFB # Packet is a keepalive packet
|
||||||
LINKCLOSE = 0xFD # Packet is a link close message
|
LINKCLOSE = 0xFC # Packet is a link close message
|
||||||
|
LINKPROOF = 0xFD # Packet is a link packet proof
|
||||||
LRRTT = 0xFE # Packet is a link request round-trip time measurement
|
LRRTT = 0xFE # Packet is a link request round-trip time measurement
|
||||||
LRPROOF = 0xFF # Packet is a link request proof
|
LRPROOF = 0xFF # Packet is a link request proof
|
||||||
|
|
||||||
@ -102,6 +103,9 @@ class Packet:
|
|||||||
elif self.packet_type == Packet.PROOF and self.context == Packet.RESOURCE_PRF:
|
elif self.packet_type == Packet.PROOF and self.context == Packet.RESOURCE_PRF:
|
||||||
# Resource proofs are not encrypted
|
# Resource proofs are not encrypted
|
||||||
self.ciphertext = self.data
|
self.ciphertext = self.data
|
||||||
|
elif self.packet_type == Packet.PROOF and self.destination.type == RNS.Destination.LINK:
|
||||||
|
# Packet proofs over links are not encrypted
|
||||||
|
self.ciphertext = self.data
|
||||||
elif self.context == Packet.RESOURCE:
|
elif self.context == Packet.RESOURCE:
|
||||||
# A resource takes care of symmetric
|
# A resource takes care of symmetric
|
||||||
# encryption by itself
|
# encryption by itself
|
||||||
@ -187,9 +191,13 @@ class Packet:
|
|||||||
raise IOError("Packet was not sent yet")
|
raise IOError("Packet was not sent yet")
|
||||||
|
|
||||||
def prove(self, destination=None):
|
def prove(self, destination=None):
|
||||||
if self.fromPacked and self.destination:
|
if self.fromPacked and hasattr(self, "destination") and self.destination:
|
||||||
if self.destination.identity and self.destination.identity.prv:
|
if self.destination.identity and self.destination.identity.prv:
|
||||||
self.destination.identity.prove(self, destination)
|
self.destination.identity.prove(self, destination)
|
||||||
|
elif self.fromPacked and hasattr(self, "link") and self.link:
|
||||||
|
self.link.prove_packet(self)
|
||||||
|
else:
|
||||||
|
RNS.log("Could not prove packet associated with neither a destination nor a link", RNS.LOG_ERROR)
|
||||||
|
|
||||||
# Generates a special destination that allows Reticulum
|
# Generates a special destination that allows Reticulum
|
||||||
# to direct the proof back to the proved packet's sender
|
# to direct the proof back to the proved packet's sender
|
||||||
@ -246,7 +254,48 @@ class PacketReceipt:
|
|||||||
|
|
||||||
# Validate a proof packet
|
# Validate a proof packet
|
||||||
def validateProofPacket(self, proof_packet):
|
def validateProofPacket(self, proof_packet):
|
||||||
return self.validateProof(proof_packet.data)
|
if hasattr(proof_packet, "link") and proof_packet.link:
|
||||||
|
return self.validate_link_proof(proof_packet.data, proof_packet.link)
|
||||||
|
else:
|
||||||
|
return self.validateProof(proof_packet.data)
|
||||||
|
|
||||||
|
# Validate a raw proof for a link
|
||||||
|
def validate_link_proof(self, proof, link):
|
||||||
|
# TODO: Hardcoded as explicit proofs for now
|
||||||
|
if True or len(proof) == PacketReceipt.EXPL_LENGTH:
|
||||||
|
# This is an explicit proof
|
||||||
|
proof_hash = proof[:RNS.Identity.HASHLENGTH/8]
|
||||||
|
signature = proof[RNS.Identity.HASHLENGTH/8:RNS.Identity.HASHLENGTH/8+RNS.Identity.SIGLENGTH/8]
|
||||||
|
if proof_hash == self.hash:
|
||||||
|
proof_valid = link.validate(signature, self.hash)
|
||||||
|
if proof_valid:
|
||||||
|
self.status = PacketReceipt.DELIVERED
|
||||||
|
self.proved = True
|
||||||
|
self.concluded_at = time.time()
|
||||||
|
if self.callbacks.delivery != None:
|
||||||
|
self.callbacks.delivery(self)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
elif len(proof) == PacketReceipt.IMPL_LENGTH:
|
||||||
|
pass
|
||||||
|
# signature = proof[:RNS.Identity.SIGLENGTH/8]
|
||||||
|
# proof_valid = self.link.validate(signature, self.hash)
|
||||||
|
# if proof_valid:
|
||||||
|
# self.status = PacketReceipt.DELIVERED
|
||||||
|
# self.proved = True
|
||||||
|
# self.concluded_at = time.time()
|
||||||
|
# if self.callbacks.delivery != None:
|
||||||
|
# self.callbacks.delivery(self)
|
||||||
|
# RNS.log("valid")
|
||||||
|
# return True
|
||||||
|
# else:
|
||||||
|
# RNS.log("invalid")
|
||||||
|
# return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
# Validate a raw proof
|
# Validate a raw proof
|
||||||
def validateProof(self, proof):
|
def validateProof(self, proof):
|
||||||
@ -286,11 +335,11 @@ class PacketReceipt:
|
|||||||
def rtt(self):
|
def rtt(self):
|
||||||
return self.concluded_at - self.sent_at
|
return self.concluded_at - self.sent_at
|
||||||
|
|
||||||
def isTimedOut(self):
|
def is_timed_out(self):
|
||||||
return (self.sent_at+self.timeout < time.time())
|
return (self.sent_at+self.timeout < time.time())
|
||||||
|
|
||||||
def checkTimeout(self):
|
def check_timeout(self):
|
||||||
if self.isTimedOut():
|
if self.is_timed_out():
|
||||||
self.status = PacketReceipt.FAILED
|
self.status = PacketReceipt.FAILED
|
||||||
self.concluded_at = time.time()
|
self.concluded_at = time.time()
|
||||||
if self.callbacks.timeout:
|
if self.callbacks.timeout:
|
||||||
@ -298,7 +347,7 @@ class PacketReceipt:
|
|||||||
|
|
||||||
|
|
||||||
# Set the timeout in seconds
|
# Set the timeout in seconds
|
||||||
def setTimeout(self, timeout):
|
def set_timeout(self, timeout):
|
||||||
self.timeout = float(timeout)
|
self.timeout = float(timeout)
|
||||||
|
|
||||||
# Set a function that gets called when
|
# Set a function that gets called when
|
||||||
|
@ -311,6 +311,7 @@ class Resource:
|
|||||||
self.status = Resource.CORRUPT
|
self.status = Resource.CORRUPT
|
||||||
|
|
||||||
if self.callback != None:
|
if self.callback != None:
|
||||||
|
self.link.resource_concluded(self)
|
||||||
self.callback(self)
|
self.callback(self)
|
||||||
|
|
||||||
|
|
||||||
@ -327,6 +328,7 @@ class Resource:
|
|||||||
if proof_data[RNS.Identity.HASHLENGTH/8:] == self.expected_proof:
|
if proof_data[RNS.Identity.HASHLENGTH/8:] == self.expected_proof:
|
||||||
self.status = Resource.COMPLETE
|
self.status = Resource.COMPLETE
|
||||||
if self.callback != None:
|
if self.callback != None:
|
||||||
|
self.link.resource_concluded(self)
|
||||||
self.callback(self)
|
self.callback(self)
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
@ -487,6 +489,7 @@ class Resource:
|
|||||||
self.link.cancel_incoming_resource(self)
|
self.link.cancel_incoming_resource(self)
|
||||||
|
|
||||||
if self.callback != None:
|
if self.callback != None:
|
||||||
|
self.link.resource_concluded(self)
|
||||||
self.callback(self)
|
self.callback(self)
|
||||||
|
|
||||||
def progress_callback(self, callback):
|
def progress_callback(self, callback):
|
||||||
|
@ -73,7 +73,7 @@ class Transport:
|
|||||||
# Process receipts list for timed-out packets
|
# Process receipts list for timed-out packets
|
||||||
if Transport.receipts_last_checked+Transport.receipts_check_interval < time.time():
|
if Transport.receipts_last_checked+Transport.receipts_check_interval < time.time():
|
||||||
for receipt in Transport.receipts:
|
for receipt in Transport.receipts:
|
||||||
receipt.checkTimeout()
|
receipt.check_timeout()
|
||||||
if receipt.status != RNS.PacketReceipt.SENT:
|
if receipt.status != RNS.PacketReceipt.SENT:
|
||||||
Transport.receipts.remove(receipt)
|
Transport.receipts.remove(receipt)
|
||||||
|
|
||||||
@ -127,6 +127,7 @@ class Transport:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def packet_filter(packet):
|
def packet_filter(packet):
|
||||||
|
# TODO: Think long and hard about this
|
||||||
if packet.context == RNS.Packet.KEEPALIVE:
|
if packet.context == RNS.Packet.KEEPALIVE:
|
||||||
return True
|
return True
|
||||||
if packet.context == RNS.Packet.RESOURCE_REQ:
|
if packet.context == RNS.Packet.RESOURCE_REQ:
|
||||||
@ -184,9 +185,10 @@ class Transport:
|
|||||||
if destination.proof_strategy == RNS.Destination.PROVE_ALL:
|
if destination.proof_strategy == RNS.Destination.PROVE_ALL:
|
||||||
packet.prove()
|
packet.prove()
|
||||||
|
|
||||||
if destination.proof_strategy == RNS.Destination.PROVE_APP:
|
elif destination.proof_strategy == RNS.Destination.PROVE_APP:
|
||||||
if destination.callbacks.proof_requested:
|
if destination.callbacks.proof_requested:
|
||||||
destination.callbacks.proof_requested(packet)
|
if destination.callbacks.proof_requested(packet):
|
||||||
|
packet.prove()
|
||||||
|
|
||||||
elif packet.packet_type == RNS.Packet.PROOF:
|
elif packet.packet_type == RNS.Packet.PROOF:
|
||||||
if packet.context == RNS.Packet.LRPROOF:
|
if packet.context == RNS.Packet.LRPROOF:
|
||||||
@ -200,6 +202,13 @@ class Transport:
|
|||||||
if link.link_id == packet.destination_hash:
|
if link.link_id == packet.destination_hash:
|
||||||
link.receive(packet)
|
link.receive(packet)
|
||||||
else:
|
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)
|
||||||
|
|
||||||
|
|
||||||
# TODO: Make sure everything uses new proof handling
|
# TODO: Make sure everything uses new proof handling
|
||||||
if len(packet.data) == RNS.PacketReceipt.EXPL_LENGTH:
|
if len(packet.data) == RNS.PacketReceipt.EXPL_LENGTH:
|
||||||
proof_hash = packet.data[:RNS.Identity.HASHLENGTH/8]
|
proof_hash = packet.data[:RNS.Identity.HASHLENGTH/8]
|
||||||
@ -230,7 +239,7 @@ class Transport:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def registerLink(link):
|
def registerLink(link):
|
||||||
RNS.log("Registering link "+str(link))
|
RNS.log("Registering link "+str(link), RNS.LOG_DEBUG)
|
||||||
if link.initiator:
|
if link.initiator:
|
||||||
Transport.pending_links.append(link)
|
Transport.pending_links.append(link)
|
||||||
else:
|
else:
|
||||||
@ -238,7 +247,7 @@ class Transport:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def activateLink(link):
|
def activateLink(link):
|
||||||
RNS.log("Activating link "+str(link))
|
RNS.log("Activating link "+str(link), RNS.LOG_DEBUG)
|
||||||
if link in Transport.pending_links:
|
if link in Transport.pending_links:
|
||||||
Transport.pending_links.remove(link)
|
Transport.pending_links.remove(link)
|
||||||
Transport.active_links.append(link)
|
Transport.active_links.append(link)
|
||||||
|
Loading…
Reference in New Issue
Block a user