diff --git a/Examples/Broadcast.py b/Examples/Broadcast.py index 29aa5ce..7bce55c 100644 --- a/Examples/Broadcast.py +++ b/Examples/Broadcast.py @@ -15,52 +15,52 @@ APP_NAME = "example_utilitites" # This initialisation is executed when the program is started def program_setup(configpath, channel=None): - # We must first initialise Reticulum - reticulum = RNS.Reticulum(configpath) - - # If the user did not select a "channel" we use - # a default one called "public_information". - # This "channel" is added to the destination name- - # space, so the user can select different broadcast - # channels. - if channel == None: - channel = "public_information" + # We must first initialise Reticulum + reticulum = RNS.Reticulum(configpath) + + # If the user did not select a "channel" we use + # a default one called "public_information". + # This "channel" is added to the destination name- + # space, so the user can select different broadcast + # channels. + if channel == None: + channel = "public_information" - # We create a PLAIN destination. This is an uncencrypted endpoint - # that anyone can listen to and send information to. - broadcast_destination = RNS.Destination(None, RNS.Destination.IN, RNS.Destination.PLAIN, APP_NAME, "broadcast", channel) + # We create a PLAIN destination. This is an uncencrypted endpoint + # that anyone can listen to and send information to. + broadcast_destination = RNS.Destination(None, RNS.Destination.IN, RNS.Destination.PLAIN, APP_NAME, "broadcast", channel) - # We specify a callback that will get called every time - # the destination receives data. - broadcast_destination.packet_callback(packet_callback) - - # Everything's ready! - # Let's hand over control to the main loop - broadcastLoop(broadcast_destination) + # We specify a callback that will get called every time + # the destination receives data. + broadcast_destination.packet_callback(packet_callback) + + # Everything's ready! + # Let's hand over control to the main loop + broadcastLoop(broadcast_destination) def packet_callback(data, packet): - # Simply print out the received data - print("") - print("Received data: "+data.decode("utf-8")+"\r\n> ", end="") - sys.stdout.flush() + # Simply print out the received data + print("") + print("Received data: "+data.decode("utf-8")+"\r\n> ", end="") + sys.stdout.flush() def broadcastLoop(destination): - # Let the user know that everything is ready - RNS.log("Broadcast example "+RNS.prettyhexrep(destination.hash)+" running, enter text and hit enter to broadcast (Ctrl-C to quit)") + # Let the user know that everything is ready + RNS.log("Broadcast example "+RNS.prettyhexrep(destination.hash)+" running, enter text and hit enter to broadcast (Ctrl-C to quit)") - # We enter a loop that runs until the users exits. - # If the user hits enter, we will send the information - # that the user entered into the prompt. - while True: - print("> ", end="") - entered = input() + # We enter a loop that runs until the users exits. + # If the user hits enter, we will send the information + # that the user entered into the prompt. + while True: + print("> ", end="") + entered = input() + + if entered != "": + data = entered.encode("utf-8") + packet = RNS.Packet(destination, data) + packet.send() - if entered != "": - data = entered.encode("utf-8") - packet = RNS.Packet(destination, data) - packet.send() - ########################################################## #### Program Startup ##################################### @@ -70,24 +70,24 @@ def broadcastLoop(destination): # and parses input from the user, and then starts # the program. if __name__ == "__main__": - try: - parser = argparse.ArgumentParser(description="Reticulum example that demonstrates sending and receiving unencrypted broadcasts") - parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str) - parser.add_argument("--channel", action="store", default=None, help="path to alternative Reticulum config directory", type=str) - args = parser.parse_args() + try: + parser = argparse.ArgumentParser(description="Reticulum example that demonstrates sending and receiving unencrypted broadcasts") + parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str) + parser.add_argument("--channel", action="store", default=None, help="path to alternative Reticulum config directory", type=str) + args = parser.parse_args() - if args.config: - configarg = args.config - else: - configarg = None + if args.config: + configarg = args.config + else: + configarg = None - if args.channel: - channelarg = args.channel - else: - channelarg = None + if args.channel: + channelarg = args.channel + else: + channelarg = None - program_setup(configarg, channelarg) + program_setup(configarg, channelarg) - except KeyboardInterrupt: - print("") - exit() \ No newline at end of file + except KeyboardInterrupt: + print("") + exit() \ No newline at end of file diff --git a/Examples/Echo.py b/Examples/Echo.py index a56e19d..7f381e9 100644 --- a/Examples/Echo.py +++ b/Examples/Echo.py @@ -22,56 +22,56 @@ APP_NAME = "example_utilitites" # This initialisation is executed when the users chooses # to run as a server def server(configpath): - # We must first initialise Reticulum - reticulum = RNS.Reticulum(configpath) - - # Randomly create a new identity for our echo server - server_identity = RNS.Identity() + # We must first initialise Reticulum + reticulum = RNS.Reticulum(configpath) + + # Randomly create a new identity for our echo server + server_identity = RNS.Identity() - # We create a destination that clients can query. We want - # to be able to verify echo replies to our clients, so we - # create a "single" destination that can receive encrypted - # messages. This way the client can send a request and be - # certain that no-one else than this destination was able - # to read it. - echo_destination = RNS.Destination(server_identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "echo", "request") + # We create a destination that clients can query. We want + # to be able to verify echo replies to our clients, so we + # create a "single" destination that can receive encrypted + # messages. This way the client can send a request and be + # certain that no-one else than this destination was able + # to read it. + echo_destination = RNS.Destination(server_identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "echo", "request") - # We configure the destination to automatically prove all - # packets adressed to it. By doing this, RNS will automatically - # generate a proof for each incoming packet and transmit it - # back to the sender of that packet. - echo_destination.set_proof_strategy(RNS.Destination.PROVE_ALL) - - # Tell the destination which function in our program to - # run when a packet is received. We do this so we can - # print a log message when the server receives a request - echo_destination.packet_callback(server_callback) + # We configure the destination to automatically prove all + # packets adressed to it. By doing this, RNS will automatically + # generate a proof for each incoming packet and transmit it + # back to the sender of that packet. + echo_destination.set_proof_strategy(RNS.Destination.PROVE_ALL) + + # Tell the destination which function in our program to + # run when a packet is received. We do this so we can + # print a log message when the server receives a request + echo_destination.packet_callback(server_callback) - # Everything's ready! - # Let's Wait for client requests or user input - announceLoop(echo_destination) + # Everything's ready! + # Let's Wait for client requests or user input + announceLoop(echo_destination) def announceLoop(destination): - # Let the user know that everything is ready - RNS.log("Echo server "+RNS.prettyhexrep(destination.hash)+" running, hit enter to manually send an announce (Ctrl-C to quit)") + # Let the user know that everything is ready + RNS.log("Echo server "+RNS.prettyhexrep(destination.hash)+" running, hit enter to manually send an announce (Ctrl-C to quit)") - # We enter a loop that runs until the users exits. - # If the user hits enter, we will announce our server - # destination on the network, which will let clients - # know how to create messages directed towards it. - while True: - entered = input() - destination.announce() - RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) + # We enter a loop that runs until the users exits. + # If the user hits enter, we will announce our server + # destination on the network, which will let clients + # know how to create messages directed towards it. + while True: + entered = input() + destination.announce() + RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) def server_callback(message, packet): - # Tell the user that we received an echo request, and - # that we are going to send a reply to the requester. - # Sending the proof is handled automatically, since we - # set up the destination to prove all incoming packets. - RNS.log("Received packet from echo client, proof sent") + # Tell the user that we received an echo request, and + # that we are going to send a reply to the requester. + # Sending the proof is handled automatically, since we + # set up the destination to prove all incoming packets. + RNS.log("Received packet from echo client, proof sent") ########################################################## @@ -81,103 +81,103 @@ def server_callback(message, packet): # This initialisation is executed when the users chooses # to run as a client def client(destination_hexhash, configpath, timeout=None): - # We need a binary representation of the destination - # hash that was entered on the command line - try: - if len(destination_hexhash) != 20: - raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)") - destination_hash = bytes.fromhex(destination_hexhash) - except: - RNS.log("Invalid destination entered. Check your input!\n") - exit() + # We need a binary representation of the destination + # hash that was entered on the command line + try: + if len(destination_hexhash) != 20: + raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)") + destination_hash = bytes.fromhex(destination_hexhash) + except: + RNS.log("Invalid destination entered. Check your input!\n") + exit() - # We must first initialise Reticulum - reticulum = RNS.Reticulum(configpath) + # We must first initialise Reticulum + reticulum = RNS.Reticulum(configpath) - # We override the loglevel to provide feedback when - # an announce is received - if RNS.loglevel < RNS.LOG_INFO: - RNS.loglevel = RNS.LOG_INFO + # We override the loglevel to provide feedback when + # an announce is received + if RNS.loglevel < RNS.LOG_INFO: + RNS.loglevel = RNS.LOG_INFO - # Tell the user that the client is ready! - RNS.log("Echo client ready, hit enter to send echo request to "+destination_hexhash+" (Ctrl-C to quit)") + # Tell the user that the client is ready! + RNS.log("Echo client ready, hit enter to send echo request to "+destination_hexhash+" (Ctrl-C to quit)") - # We enter a loop that runs until the user exits. - # If the user hits enter, we will try to send an - # echo request to the destination specified on the - # command line. - while True: - input() - - # Let's first check if RNS knows a path to the destination. - # If it does, we'll load the server identity and create a packet - if RNS.Transport.hasPath(destination_hash): + # We enter a loop that runs until the user exits. + # If the user hits enter, we will try to send an + # echo request to the destination specified on the + # command line. + while True: + input() + + # Let's first check if RNS knows a path to the destination. + # If it does, we'll load the server identity and create a packet + if RNS.Transport.hasPath(destination_hash): - # To address the server, we need to know it's public - # key, so we check if Reticulum knows this destination. - # This is done by calling the "recall" method of the - # Identity module. If the destination is known, it will - # return an Identity instance that can be used in - # outgoing destinations. - server_identity = RNS.Identity.recall(destination_hash) + # To address the server, we need to know it's public + # key, so we check if Reticulum knows this destination. + # This is done by calling the "recall" method of the + # Identity module. If the destination is known, it will + # return an Identity instance that can be used in + # outgoing destinations. + server_identity = RNS.Identity.recall(destination_hash) - # We got the correct identity instance from the - # recall method, so let's create an outgoing - # destination. We use the naming convention: - # example_utilities.echo.request - # This matches the naming we specified in the - # server part of the code. - request_destination = RNS.Destination(server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "echo", "request") + # We got the correct identity instance from the + # recall method, so let's create an outgoing + # destination. We use the naming convention: + # example_utilities.echo.request + # This matches the naming we specified in the + # server part of the code. + request_destination = RNS.Destination(server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "echo", "request") - # The destination is ready, so let's create a packet. - # We set the destination to the request_destination - # that was just created, and the only data we add - # is a random hash. - echo_request = RNS.Packet(request_destination, RNS.Identity.getRandomHash()) + # The destination is ready, so let's create a packet. + # We set the destination to the request_destination + # that was just created, and the only data we add + # is a random hash. + echo_request = RNS.Packet(request_destination, RNS.Identity.getRandomHash()) - # Send the packet! If the packet is successfully - # sent, it will return a PacketReceipt instance. - packet_receipt = echo_request.send() + # Send the packet! If the packet is successfully + # sent, it will return a PacketReceipt instance. + packet_receipt = echo_request.send() - # If the user specified a timeout, we set this - # timeout on the packet receipt, and configure - # a callback function, that will get called if - # the packet times out. - if timeout != None: - packet_receipt.set_timeout(timeout) - packet_receipt.timeout_callback(packet_timed_out) + # If the user specified a timeout, we set this + # timeout on the packet receipt, and configure + # a callback function, that will get called if + # the packet times out. + if timeout != None: + packet_receipt.set_timeout(timeout) + packet_receipt.timeout_callback(packet_timed_out) - # We can then set a delivery callback on the receipt. - # This will get automatically called when a proof for - # this specific packet is received from the destination. - packet_receipt.delivery_callback(packet_delivered) + # We can then set a delivery callback on the receipt. + # This will get automatically called when a proof for + # this specific packet is received from the destination. + packet_receipt.delivery_callback(packet_delivered) - # Tell the user that the echo request was sent - RNS.log("Sent echo request to "+RNS.prettyhexrep(request_destination.hash)) - else: - # If we do not know this destination, tell the - # user to wait for an announce to arrive. - RNS.log("Destination is not yet known. Requesting path...") - RNS.Transport.requestPath(destination_hash) + # Tell the user that the echo request was sent + RNS.log("Sent echo request to "+RNS.prettyhexrep(request_destination.hash)) + else: + # If we do not know this destination, tell the + # user to wait for an announce to arrive. + RNS.log("Destination is not yet known. Requesting path...") + RNS.Transport.requestPath(destination_hash) # This function is called when our reply destination # receives a proof packet. def packet_delivered(receipt): - if receipt.status == RNS.PacketReceipt.DELIVERED: - rtt = receipt.rtt() - if (rtt >= 1): - rtt = round(rtt, 3) - rttstring = str(rtt)+" seconds" - else: - rtt = round(rtt*1000, 3) - rttstring = str(rtt)+" milliseconds" + if receipt.status == RNS.PacketReceipt.DELIVERED: + rtt = receipt.rtt() + if (rtt >= 1): + rtt = round(rtt, 3) + rttstring = str(rtt)+" seconds" + else: + rtt = round(rtt*1000, 3) + rttstring = str(rtt)+" milliseconds" - RNS.log("Valid reply received from "+RNS.prettyhexrep(receipt.destination.hash)+", round-trip time is "+rttstring) + RNS.log("Valid reply received from "+RNS.prettyhexrep(receipt.destination.hash)+", round-trip time is "+rttstring) # This function is called if a packet times out. def packet_timed_out(receipt): - if receipt.status == RNS.PacketReceipt.FAILED: - RNS.log("Packet "+RNS.prettyhexrep(receipt.hash)+" timed out") + if receipt.status == RNS.PacketReceipt.FAILED: + RNS.log("Packet "+RNS.prettyhexrep(receipt.hash)+" timed out") ########################################################## @@ -188,36 +188,36 @@ def packet_timed_out(receipt): # and parses input from the user, and then starts # the desired program mode. if __name__ == "__main__": - try: - parser = argparse.ArgumentParser(description="Simple echo server and client utility") - parser.add_argument("-s", "--server", action="store_true", help="wait for incoming packets from clients") - parser.add_argument("-t", "--timeout", action="store", metavar="s", default=None, help="set a reply timeout in seconds", type=float) - parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str) - parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the server destination", type=str) - args = parser.parse_args() + try: + parser = argparse.ArgumentParser(description="Simple echo server and client utility") + parser.add_argument("-s", "--server", action="store_true", help="wait for incoming packets from clients") + parser.add_argument("-t", "--timeout", action="store", metavar="s", default=None, help="set a reply timeout in seconds", type=float) + parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str) + parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the server destination", type=str) + args = parser.parse_args() - if args.server: - configarg=None - if args.config: - configarg = args.config - server(configarg) - else: - if args.config: - configarg = args.config - else: - configarg = None + if args.server: + configarg=None + if args.config: + configarg = args.config + server(configarg) + else: + if args.config: + configarg = args.config + else: + configarg = None - if args.timeout: - timeoutarg = float(args.timeout) - else: - timeoutarg = None + if args.timeout: + timeoutarg = float(args.timeout) + else: + timeoutarg = None - if (args.destination == None): - print("") - parser.print_help() - print("") - else: - client(args.destination, configarg, timeout=timeoutarg) - except KeyboardInterrupt: - print("") - exit() \ No newline at end of file + if (args.destination == None): + print("") + parser.print_help() + print("") + else: + client(args.destination, configarg, timeout=timeoutarg) + except KeyboardInterrupt: + print("") + exit() \ No newline at end of file diff --git a/Examples/Filetransfer.py b/Examples/Filetransfer.py index 9304b0d..6fd9cfa 100644 --- a/Examples/Filetransfer.py +++ b/Examples/Filetransfer.py @@ -42,132 +42,132 @@ serve_path = None # This initialisation is executed when the users chooses # to run as a server def server(configpath, path): - # We must first initialise Reticulum - reticulum = RNS.Reticulum(configpath) - - # Randomly create a new identity for our file server - server_identity = RNS.Identity() + # We must first initialise Reticulum + reticulum = RNS.Reticulum(configpath) + + # Randomly create a new identity for our file server + server_identity = RNS.Identity() - global serve_path - serve_path = path + global serve_path + serve_path = path - # We create a destination that clients can connect to. We - # want clients to create links to this destination, so we - # need to create a "single" destination type. - server_destination = RNS.Destination(server_identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "filetransfer", "server") + # We create a destination that clients can connect to. We + # want clients to create links to this destination, so we + # need to create a "single" destination type. + server_destination = RNS.Destination(server_identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "filetransfer", "server") - # We configure a function that will get called every time - # a new client creates a link to this destination. - server_destination.link_established_callback(client_connected) + # We configure a function that will get called every time + # a new client creates a link to this destination. + server_destination.link_established_callback(client_connected) - # Everything's ready! - # Let's Wait for client requests or user input - announceLoop(server_destination) + # Everything's ready! + # Let's Wait for client requests or user input + announceLoop(server_destination) def announceLoop(destination): - # Let the user know that everything is ready - RNS.log("File server "+RNS.prettyhexrep(destination.hash)+" running") - RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)") + # Let the user know that everything is ready + RNS.log("File server "+RNS.prettyhexrep(destination.hash)+" running") + RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)") - # We enter a loop that runs until the users exits. - # If the user hits enter, we will announce our server - # destination on the network, which will let clients - # know how to create messages directed towards it. - while True: - entered = input() - destination.announce() - RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) + # We enter a loop that runs until the users exits. + # If the user hits enter, we will announce our server + # destination on the network, which will let clients + # know how to create messages directed towards it. + while True: + entered = input() + destination.announce() + RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) # Here's a convenience function for listing all files # in our served directory def list_files(): - # We add all entries from the directory that are - # actual files, and does not start with "." - global serve_path - return [file for file in os.listdir(serve_path) if os.path.isfile(os.path.join(serve_path, file)) and file[:1] != "."] + # We add all entries from the directory that are + # actual files, and does not start with "." + global serve_path + return [file for file in os.listdir(serve_path) if os.path.isfile(os.path.join(serve_path, file)) and file[:1] != "."] # When a client establishes a link to our server # destination, this function will be called with # a reference to the link. We then send the client # a list of files hosted on the server. def client_connected(link): - # Check if the served directory still exists - if os.path.isdir(serve_path): - RNS.log("Client connected, sending file list...") + # Check if the served directory still exists + if os.path.isdir(serve_path): + RNS.log("Client connected, sending file list...") - link.link_closed_callback(client_disconnected) + link.link_closed_callback(client_disconnected) - # We pack a list of files for sending in a packet - data = umsgpack.packb(list_files()) + # We pack a list of files for sending in a packet + data = umsgpack.packb(list_files()) - # Check the size of the packed data - if len(data) <= RNS.Link.MDU: - # If it fits in one packet, we will just - # send it as a single packet over the link. - list_packet = RNS.Packet(link, data) - list_receipt = list_packet.send() - list_receipt.set_timeout(APP_TIMEOUT) - list_receipt.delivery_callback(list_delivered) - list_receipt.timeout_callback(list_timeout) - else: - RNS.log("Too many files in served directory!", RNS.LOG_ERROR) - RNS.log("You should implement a function to split the filelist over multiple packets.", RNS.LOG_ERROR) - RNS.log("Hint: The client already supports it :)", RNS.LOG_ERROR) - - # After this, we're just going to keep the link - # open until the client requests a file. We'll - # configure a function that get's called when - # the client sends a packet with a file request. - link.packet_callback(client_request) - else: - RNS.log("Client connected, but served path no longer exists!", RNS.LOG_ERROR) - link.teardown() + # Check the size of the packed data + if len(data) <= RNS.Link.MDU: + # If it fits in one packet, we will just + # send it as a single packet over the link. + list_packet = RNS.Packet(link, data) + list_receipt = list_packet.send() + list_receipt.set_timeout(APP_TIMEOUT) + list_receipt.delivery_callback(list_delivered) + list_receipt.timeout_callback(list_timeout) + else: + RNS.log("Too many files in served directory!", RNS.LOG_ERROR) + RNS.log("You should implement a function to split the filelist over multiple packets.", RNS.LOG_ERROR) + RNS.log("Hint: The client already supports it :)", RNS.LOG_ERROR) + + # After this, we're just going to keep the link + # open until the client requests a file. We'll + # configure a function that get's called when + # the client sends a packet with a file request. + link.packet_callback(client_request) + else: + RNS.log("Client connected, but served path no longer exists!", RNS.LOG_ERROR) + link.teardown() def client_disconnected(link): - RNS.log("Client disconnected") + RNS.log("Client disconnected") def client_request(message, packet): - global serve_path - filename = message.decode("utf-8") - if filename in list_files(): - try: - # If we have the requested file, we'll - # read it and pack it as a resource - RNS.log("Client requested \""+filename+"\"") - file = open(os.path.join(serve_path, filename), "rb") - file_resource = RNS.Resource(file, packet.link, callback=resource_sending_concluded) - file_resource.filename = filename - except Exception as e: - # If somethign went wrong, we close - # the link - RNS.log("Error while reading file \""+filename+"\"", RNS.LOG_ERROR) - packet.link.teardown() - raise e - else: - # If we don't have it, we close the link - RNS.log("Client requested an unknown file") - packet.link.teardown() + global serve_path + filename = message.decode("utf-8") + if filename in list_files(): + try: + # If we have the requested file, we'll + # read it and pack it as a resource + RNS.log("Client requested \""+filename+"\"") + file = open(os.path.join(serve_path, filename), "rb") + file_resource = RNS.Resource(file, packet.link, callback=resource_sending_concluded) + file_resource.filename = filename + except Exception as e: + # If somethign went wrong, we close + # the link + RNS.log("Error while reading file \""+filename+"\"", RNS.LOG_ERROR) + packet.link.teardown() + raise e + else: + # If we don't have it, we close the link + RNS.log("Client requested an unknown file") + packet.link.teardown() # This function is called on the server when a # resource transfer concludes. def resource_sending_concluded(resource): - if hasattr(resource, "filename"): - name = resource.filename - else: - name = "resource" + if hasattr(resource, "filename"): + name = resource.filename + else: + name = "resource" - if resource.status == RNS.Resource.COMPLETE: - RNS.log("Done sending \""+name+"\" to client") - elif resource.status == RNS.Resource.FAILED: - RNS.log("Sending \""+name+"\" to client failed") + if resource.status == RNS.Resource.COMPLETE: + RNS.log("Done sending \""+name+"\" to client") + elif resource.status == RNS.Resource.FAILED: + RNS.log("Sending \""+name+"\" to client failed") def list_delivered(receipt): - RNS.log("The file list was received by the client") + RNS.log("The file list was received by the client") def list_timeout(receipt): - RNS.log("Sending list to client timed out, closing this link") - link = receipt.destination - link.teardown() + RNS.log("Sending list to client timed out, closing this link") + link = receipt.destination + link.teardown() ########################################################## #### Client Part ######################################### @@ -194,116 +194,116 @@ file_size = 0 # This initialisation is executed when the users chooses # to run as a client def client(destination_hexhash, configpath): - # We need a binary representation of the destination - # hash that was entered on the command line - try: - if len(destination_hexhash) != 20: - raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)") - destination_hash = bytes.fromhex(destination_hexhash) - except: - RNS.log("Invalid destination entered. Check your input!\n") - exit() + # We need a binary representation of the destination + # hash that was entered on the command line + try: + if len(destination_hexhash) != 20: + raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)") + destination_hash = bytes.fromhex(destination_hexhash) + except: + RNS.log("Invalid destination entered. Check your input!\n") + exit() - # We must first initialise Reticulum - reticulum = RNS.Reticulum(configpath) + # We must first initialise Reticulum + reticulum = RNS.Reticulum(configpath) - # Check if we know a path to the destination - if not RNS.Transport.hasPath(destination_hash): - RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...") - RNS.Transport.requestPath(destination_hash) - while not RNS.Transport.hasPath(destination_hash): - time.sleep(0.1) + # Check if we know a path to the destination + if not RNS.Transport.hasPath(destination_hash): + RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...") + RNS.Transport.requestPath(destination_hash) + while not RNS.Transport.hasPath(destination_hash): + time.sleep(0.1) - # Recall the server identity - server_identity = RNS.Identity.recall(destination_hash) + # Recall the server identity + server_identity = RNS.Identity.recall(destination_hash) - # Inform the user that we'll begin connecting - RNS.log("Establishing link with server...") + # Inform the user that we'll begin connecting + RNS.log("Establishing link with server...") - # When the server identity is known, we set - # up a destination - server_destination = RNS.Destination(server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "filetransfer", "server") + # When the server identity is known, we set + # up a destination + server_destination = RNS.Destination(server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "filetransfer", "server") - # We also want to automatically prove incoming packets - server_destination.set_proof_strategy(RNS.Destination.PROVE_ALL) + # We also want to automatically prove incoming packets + server_destination.set_proof_strategy(RNS.Destination.PROVE_ALL) - # And create a link - link = RNS.Link(server_destination) + # And create a link + link = RNS.Link(server_destination) - # We expect any normal data packets on the link - # to contain a list of served files, so we set - # a callback accordingly - link.packet_callback(filelist_received) + # We expect any normal data packets on the link + # to contain a list of served files, so we set + # a callback accordingly + link.packet_callback(filelist_received) - # We'll also set up functions to inform the - # user when the link is established or closed - link.link_established_callback(link_established) - link.link_closed_callback(link_closed) + # We'll also set up functions to inform the + # user when the link is established or closed + link.link_established_callback(link_established) + link.link_closed_callback(link_closed) - # And set the link to automatically begin - # downloading advertised resources - link.set_resource_strategy(RNS.Link.ACCEPT_ALL) - link.resource_started_callback(download_began) - link.resource_concluded_callback(download_concluded) + # And set the link to automatically begin + # downloading advertised resources + link.set_resource_strategy(RNS.Link.ACCEPT_ALL) + link.resource_started_callback(download_began) + link.resource_concluded_callback(download_concluded) - menu() + menu() # Requests the specified file from the server def download(filename): - global server_link, menu_mode, current_filename, transfer_size, download_started - current_filename = filename - download_started = 0 - transfer_size = 0 + global server_link, menu_mode, current_filename, transfer_size, download_started + current_filename = filename + download_started = 0 + transfer_size = 0 - # We just create a packet containing the - # requested filename, and send it down the - # link. We also specify we don't need a - # packet receipt. - request_packet = RNS.Packet(server_link, filename.encode("utf-8"), create_receipt=False) - request_packet.send() - - print("") - print(("Requested \""+filename+"\" from server, waiting for download to begin...")) - menu_mode = "download_started" + # We just create a packet containing the + # requested filename, and send it down the + # link. We also specify we don't need a + # packet receipt. + request_packet = RNS.Packet(server_link, filename.encode("utf-8"), create_receipt=False) + request_packet.send() + + print("") + print(("Requested \""+filename+"\" from server, waiting for download to begin...")) + menu_mode = "download_started" # This function runs a simple menu for the user # to select which files to download, or quit menu_mode = None def menu(): - global server_files, server_link - # Wait until we have a filelist - while len(server_files) == 0: - time.sleep(0.1) - RNS.log("Ready!") - time.sleep(0.5) + global server_files, server_link + # Wait until we have a filelist + while len(server_files) == 0: + time.sleep(0.1) + RNS.log("Ready!") + time.sleep(0.5) - global menu_mode - menu_mode = "main" - should_quit = False - while (not should_quit): - print_menu() + global menu_mode + menu_mode = "main" + should_quit = False + while (not should_quit): + print_menu() - while not menu_mode == "main": - # Wait - time.sleep(0.25) + while not menu_mode == "main": + # Wait + time.sleep(0.25) - user_input = input() - if user_input == "q" or user_input == "quit" or user_input == "exit": - should_quit = True - print("") - else: - if user_input in server_files: - download(user_input) - else: - try: - if 0 <= int(user_input) < len(server_files): - download(server_files[int(user_input)]) - except: - pass + user_input = input() + if user_input == "q" or user_input == "quit" or user_input == "exit": + should_quit = True + print("") + else: + if user_input in server_files: + download(user_input) + else: + try: + if 0 <= int(user_input) < len(server_files): + download(server_files[int(user_input)]) + except: + pass - if should_quit: - server_link.teardown() + if should_quit: + server_link.teardown() # Prints out menus or screens for the # various states of the client program. @@ -311,145 +311,145 @@ def menu(): # I won't go into detail here. Just # strings basically. def print_menu(): - global menu_mode, download_time, download_started, download_finished, transfer_size, file_size + global menu_mode, download_time, download_started, download_finished, transfer_size, file_size - if menu_mode == "main": - clear_screen() - print_filelist() - print("") - print("Select a file to download by entering name or number, or q to quit") - print(("> "), end=' ') - elif menu_mode == "download_started": - download_began = time.time() - while menu_mode == "download_started": - time.sleep(0.1) - if time.time() > download_began+APP_TIMEOUT: - print("The download timed out") - time.sleep(1) - server_link.teardown() + if menu_mode == "main": + clear_screen() + print_filelist() + print("") + print("Select a file to download by entering name or number, or q to quit") + print(("> "), end=' ') + elif menu_mode == "download_started": + download_began = time.time() + while menu_mode == "download_started": + time.sleep(0.1) + if time.time() > download_began+APP_TIMEOUT: + print("The download timed out") + time.sleep(1) + server_link.teardown() - if menu_mode == "downloading": - print("Download started") - print("") - while menu_mode == "downloading": - global current_download - percent = round(current_download.progress() * 100.0, 1) - print(("\rProgress: "+str(percent)+" % "), end=' ') - sys.stdout.flush() - time.sleep(0.1) + if menu_mode == "downloading": + print("Download started") + print("") + while menu_mode == "downloading": + global current_download + percent = round(current_download.progress() * 100.0, 1) + print(("\rProgress: "+str(percent)+" % "), end=' ') + sys.stdout.flush() + time.sleep(0.1) - if menu_mode == "save_error": - print(("\rProgress: 100.0 %"), end=' ') - sys.stdout.flush() - print("") - print("Could not write downloaded file to disk") - current_download.status = RNS.Resource.FAILED - menu_mode = "download_concluded" + if menu_mode == "save_error": + print(("\rProgress: 100.0 %"), end=' ') + sys.stdout.flush() + print("") + print("Could not write downloaded file to disk") + current_download.status = RNS.Resource.FAILED + menu_mode = "download_concluded" - if menu_mode == "download_concluded": - if current_download.status == RNS.Resource.COMPLETE: - print(("\rProgress: 100.0 %"), end=' ') - sys.stdout.flush() + if menu_mode == "download_concluded": + if current_download.status == RNS.Resource.COMPLETE: + print(("\rProgress: 100.0 %"), end=' ') + sys.stdout.flush() - # Print statistics - hours, rem = divmod(download_time, 3600) - minutes, seconds = divmod(rem, 60) - timestring = "{:0>2}:{:0>2}:{:05.2f}".format(int(hours),int(minutes),seconds) - print("") - print("") - print("--- Statistics -----") - print("\tTime taken : "+timestring) - print("\tFile size : "+size_str(file_size)) - print("\tData transferred : "+size_str(transfer_size)) - print("\tEffective rate : "+size_str(file_size/download_time, suffix='b')+"/s") - print("\tTransfer rate : "+size_str(transfer_size/download_time, suffix='b')+"/s") - print("") - print("The download completed! Press enter to return to the menu.") - print("") - input() + # Print statistics + hours, rem = divmod(download_time, 3600) + minutes, seconds = divmod(rem, 60) + timestring = "{:0>2}:{:0>2}:{:05.2f}".format(int(hours),int(minutes),seconds) + print("") + print("") + print("--- Statistics -----") + print("\tTime taken : "+timestring) + print("\tFile size : "+size_str(file_size)) + print("\tData transferred : "+size_str(transfer_size)) + print("\tEffective rate : "+size_str(file_size/download_time, suffix='b')+"/s") + print("\tTransfer rate : "+size_str(transfer_size/download_time, suffix='b')+"/s") + print("") + print("The download completed! Press enter to return to the menu.") + print("") + input() - else: - print("") - print("The download failed! Press enter to return to the menu.") - input() + else: + print("") + print("The download failed! Press enter to return to the menu.") + input() - current_download = None - menu_mode = "main" - print_menu() + current_download = None + menu_mode = "main" + print_menu() # This function prints out a list of files # on the connected server. def print_filelist(): - global server_files - print("Files on server:") - for index,file in enumerate(server_files): - print("\t("+str(index)+")\t"+file) + global server_files + print("Files on server:") + for index,file in enumerate(server_files): + print("\t("+str(index)+")\t"+file) def filelist_received(filelist_data, packet): - global server_files, menu_mode - try: - # Unpack the list and extend our - # local list of available files - filelist = umsgpack.unpackb(filelist_data) - for file in filelist: - if not file in server_files: - server_files.append(file) + global server_files, menu_mode + try: + # Unpack the list and extend our + # local list of available files + filelist = umsgpack.unpackb(filelist_data) + for file in filelist: + if not file in server_files: + server_files.append(file) - # If the menu is already visible, - # we'll update it with what was - # just received - if menu_mode == "main": - print_menu() - except: - RNS.log("Invalid file list data received, closing link") - packet.link.teardown() + # If the menu is already visible, + # we'll update it with what was + # just received + if menu_mode == "main": + print_menu() + except: + RNS.log("Invalid file list data received, closing link") + packet.link.teardown() # This function is called when a link # has been established with the server def link_established(link): - # We store a reference to the link - # instance for later use - global server_link - server_link = link + # We store a reference to the link + # instance for later use + global server_link + server_link = link - # Inform the user that the server is - # connected - RNS.log("Link established with server") - RNS.log("Waiting for filelist...") + # Inform the user that the server is + # connected + RNS.log("Link established with server") + RNS.log("Waiting for filelist...") - # And set up a small job to check for - # a potential timeout in receiving the - # file list - thread = threading.Thread(target=filelist_timeout_job) - thread.setDaemon(True) - thread.start() + # And set up a small job to check for + # a potential timeout in receiving the + # file list + thread = threading.Thread(target=filelist_timeout_job) + thread.setDaemon(True) + thread.start() # This job just sleeps for the specified # time, and then checks if the file list # was received. If not, the program will # exit. def filelist_timeout_job(): - time.sleep(APP_TIMEOUT) + time.sleep(APP_TIMEOUT) - global server_files - if len(server_files) == 0: - RNS.log("Timed out waiting for filelist, exiting") - os._exit(0) + global server_files + if len(server_files) == 0: + RNS.log("Timed out waiting for filelist, exiting") + os._exit(0) # When a link is closed, we'll inform the # user, and exit the program def link_closed(link): - if link.teardown_reason == RNS.Link.TIMEOUT: - RNS.log("The link timed out, exiting now") - elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED: - RNS.log("The link was closed by the server, exiting now") - else: - RNS.log("Link closed, exiting now") - - RNS.Reticulum.exit_handler() - time.sleep(1.5) - os._exit(0) + if link.teardown_reason == RNS.Link.TIMEOUT: + RNS.log("The link timed out, exiting now") + elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED: + RNS.log("The link was closed by the server, exiting now") + else: + RNS.log("Link closed, exiting now") + + RNS.Reticulum.exit_handler() + time.sleep(1.5) + os._exit(0) # When RNS detects that the download has # started, we'll update our menu state @@ -471,45 +471,45 @@ def download_began(resource): # or not, we'll update our menu state and # inform the user about how it all went. def download_concluded(resource): - global menu_mode, current_filename, download_started, download_finished, download_time - download_finished = time.time() - download_time = download_finished - download_started + global menu_mode, current_filename, download_started, download_finished, download_time + download_finished = time.time() + download_time = download_finished - download_started - saved_filename = current_filename + saved_filename = current_filename - if resource.status == RNS.Resource.COMPLETE: - counter = 0 - while os.path.isfile(saved_filename): - counter += 1 - saved_filename = current_filename+"."+str(counter) + if resource.status == RNS.Resource.COMPLETE: + counter = 0 + while os.path.isfile(saved_filename): + counter += 1 + saved_filename = current_filename+"."+str(counter) - try: - file = open(saved_filename, "wb") - file.write(resource.data.read()) - file.close() - menu_mode = "download_concluded" - except: - menu_mode = "save_error" - else: - menu_mode = "download_concluded" + try: + file = open(saved_filename, "wb") + file.write(resource.data.read()) + file.close() + menu_mode = "download_concluded" + except: + menu_mode = "save_error" + else: + menu_mode = "download_concluded" # A convenience function for printing a human- # readable file size def size_str(num, suffix='B'): - units = ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi'] - last_unit = 'Yi' + units = ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi'] + last_unit = 'Yi' - if suffix == 'b': - num *= 8 - units = ['','K','M','G','T','P','E','Z'] - last_unit = 'Y' + if suffix == 'b': + num *= 8 + units = ['','K','M','G','T','P','E','Z'] + last_unit = 'Y' - for unit in units: - if abs(num) < 1024.0: - return "%3.2f %s%s" % (num, unit, suffix) - num /= 1024.0 - return "%.2f %s%s" % (num, last_unit, suffix) + for unit in units: + if abs(num) < 1024.0: + return "%3.2f %s%s" % (num, unit, suffix) + num /= 1024.0 + return "%.2f %s%s" % (num, last_unit, suffix) # A convenience function for clearing the screen def clear_screen(): @@ -523,31 +523,31 @@ def clear_screen(): # and parses input of from the user, and then # starts up the desired program mode. if __name__ == "__main__": - try: - parser = argparse.ArgumentParser(description="Simple file transfer server and client utility") - parser.add_argument("-s", "--serve", action="store", metavar="dir", help="serve a directory of files to clients") - parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str) - parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the server destination", type=str) - args = parser.parse_args() + try: + parser = argparse.ArgumentParser(description="Simple file transfer server and client utility") + parser.add_argument("-s", "--serve", action="store", metavar="dir", help="serve a directory of files to clients") + parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str) + parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the server destination", type=str) + args = parser.parse_args() - if args.config: - configarg = args.config - else: - configarg = None + if args.config: + configarg = args.config + else: + configarg = None - if args.serve: - if os.path.isdir(args.serve): - server(configarg, args.serve) - else: - RNS.log("The specified directory does not exist") - else: - if (args.destination == None): - print("") - parser.print_help() - print("") - else: - client(args.destination, configarg) + if args.serve: + if os.path.isdir(args.serve): + server(configarg, args.serve) + else: + RNS.log("The specified directory does not exist") + else: + if (args.destination == None): + print("") + parser.print_help() + print("") + else: + client(args.destination, configarg) - except KeyboardInterrupt: - print("") - exit() \ No newline at end of file + except KeyboardInterrupt: + print("") + exit() \ No newline at end of file diff --git a/Examples/Link.py b/Examples/Link.py index 9800406..f915bc4 100644 --- a/Examples/Link.py +++ b/Examples/Link.py @@ -25,65 +25,65 @@ latest_client_link = None # This initialisation is executed when the users chooses # to run as a server def server(configpath): - # We must first initialise Reticulum - reticulum = RNS.Reticulum(configpath) - - # Randomly create a new identity for our link example - server_identity = RNS.Identity() + # We must first initialise Reticulum + reticulum = RNS.Reticulum(configpath) + + # Randomly create a new identity for our link example + server_identity = RNS.Identity() - # We create a destination that clients can connect to. We - # want clients to create links to this destination, so we - # need to create a "single" destination type. - server_destination = RNS.Destination(server_identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "linkexample") + # We create a destination that clients can connect to. We + # want clients to create links to this destination, so we + # need to create a "single" destination type. + server_destination = RNS.Destination(server_identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "linkexample") - # We configure a function that will get called every time - # a new client creates a link to this destination. - server_destination.link_established_callback(client_connected) + # We configure a function that will get called every time + # a new client creates a link to this destination. + server_destination.link_established_callback(client_connected) - # Everything's ready! - # Let's Wait for client requests or user input - server_loop(server_destination) + # Everything's ready! + # Let's Wait for client requests or user input + server_loop(server_destination) def server_loop(destination): - # Let the user know that everything is ready - RNS.log("Link example "+RNS.prettyhexrep(destination.hash)+" running, waiting for a connection.") - RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)") + # Let the user know that everything is ready + RNS.log("Link example "+RNS.prettyhexrep(destination.hash)+" running, waiting for a connection.") + RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)") - # We enter a loop that runs until the users exits. - # If the user hits enter, we will announce our server - # destination on the network, which will let clients - # know how to create messages directed towards it. - while True: - entered = input() - destination.announce() - RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) + # We enter a loop that runs until the users exits. + # If the user hits enter, we will announce our server + # destination on the network, which will let clients + # know how to create messages directed towards it. + while True: + entered = input() + destination.announce() + RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) # When a client establishes a link to our server # destination, this function will be called with # a reference to the link. def client_connected(link): - global latest_client_link + global latest_client_link - RNS.log("Client connected") - link.link_closed_callback(client_disconnected) - link.packet_callback(server_packet_received) - latest_client_link = link + RNS.log("Client connected") + link.link_closed_callback(client_disconnected) + link.packet_callback(server_packet_received) + latest_client_link = link def client_disconnected(link): - RNS.log("Client disconnected") + RNS.log("Client disconnected") def server_packet_received(message, packet): - global latest_client_link + global latest_client_link - # When data is received over any active link, - # it will all be directed to the last client - # that connected. - text = message.decode("utf-8") - RNS.log("Received data on the link: "+text) - - reply_text = "I received \""+text+"\" over the link" - reply_data = reply_text.encode("utf-8") - RNS.Packet(latest_client_link, reply_data).send() + # When data is received over any active link, + # it will all be directed to the last client + # that connected. + text = message.decode("utf-8") + RNS.log("Received data on the link: "+text) + + reply_text = "I received \""+text+"\" over the link" + reply_data = reply_text.encode("utf-8") + RNS.Packet(latest_client_link, reply_data).send() ########################################################## @@ -96,112 +96,112 @@ server_link = None # This initialisation is executed when the users chooses # to run as a client def client(destination_hexhash, configpath): - # We need a binary representation of the destination - # hash that was entered on the command line - try: - if len(destination_hexhash) != 20: - raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)") - destination_hash = bytes.fromhex(destination_hexhash) - except: - RNS.log("Invalid destination entered. Check your input!\n") - exit() + # We need a binary representation of the destination + # hash that was entered on the command line + try: + if len(destination_hexhash) != 20: + raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)") + destination_hash = bytes.fromhex(destination_hexhash) + except: + RNS.log("Invalid destination entered. Check your input!\n") + exit() - # We must first initialise Reticulum - reticulum = RNS.Reticulum(configpath) + # We must first initialise Reticulum + reticulum = RNS.Reticulum(configpath) - # Check if we know a path to the destination - if not RNS.Transport.hasPath(destination_hash): - RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...") - RNS.Transport.requestPath(destination_hash) - while not RNS.Transport.hasPath(destination_hash): - time.sleep(0.1) + # Check if we know a path to the destination + if not RNS.Transport.hasPath(destination_hash): + RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...") + RNS.Transport.requestPath(destination_hash) + while not RNS.Transport.hasPath(destination_hash): + time.sleep(0.1) - # Recall the server identity - server_identity = RNS.Identity.recall(destination_hash) + # Recall the server identity + server_identity = RNS.Identity.recall(destination_hash) - # Inform the user that we'll begin connecting - RNS.log("Establishing link with server...") + # Inform the user that we'll begin connecting + RNS.log("Establishing link with server...") - # When the server identity is known, we set - # up a destination - server_destination = RNS.Destination(server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "linkexample") + # When the server identity is known, we set + # up a destination + server_destination = RNS.Destination(server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "linkexample") - # And create a link - link = RNS.Link(server_destination) + # And create a link + link = RNS.Link(server_destination) - # We set a callback that will get executed - # every time a packet is received over the - # link - link.packet_callback(client_packet_received) + # We set a callback that will get executed + # every time a packet is received over the + # link + link.packet_callback(client_packet_received) - # We'll also set up functions to inform the - # user when the link is established or closed - link.link_established_callback(link_established) - link.link_closed_callback(link_closed) + # We'll also set up functions to inform the + # user when the link is established or closed + link.link_established_callback(link_established) + link.link_closed_callback(link_closed) - # Everything is set up, so let's enter a loop - # for the user to interact with the example - client_loop() + # Everything is set up, so let's enter a loop + # for the user to interact with the example + client_loop() def client_loop(): - global server_link + global server_link - # Wait for the link to become active - while not server_link: - time.sleep(0.1) + # Wait for the link to become active + while not server_link: + time.sleep(0.1) - should_quit = False - while not should_quit: - try: - print("> ", end=" ") - text = input() + should_quit = False + while not should_quit: + try: + print("> ", end=" ") + text = input() - # Check if we should quit the example - if text == "quit" or text == "q" or text == "exit": - should_quit = True - server_link.teardown() + # Check if we should quit the example + if text == "quit" or text == "q" or text == "exit": + should_quit = True + server_link.teardown() - # If not, send the entered text over the link - if text != "": - data = text.encode("utf-8") - RNS.Packet(server_link, data).send() - except Exception as e: - should_quit = True - server_link.teardown() + # If not, send the entered text over the link + if text != "": + data = text.encode("utf-8") + RNS.Packet(server_link, data).send() + except Exception as e: + should_quit = True + server_link.teardown() # This function is called when a link # has been established with the server def link_established(link): - # We store a reference to the link - # instance for later use - global server_link - server_link = link + # We store a reference to the link + # instance for later use + global server_link + server_link = link - # Inform the user that the server is - # connected - RNS.log("Link established with server, enter some text to send, or \"quit\" to quit") + # Inform the user that the server is + # connected + RNS.log("Link established with server, enter some text to send, or \"quit\" to quit") # When a link is closed, we'll inform the # user, and exit the program def link_closed(link): - if link.teardown_reason == RNS.Link.TIMEOUT: - RNS.log("The link timed out, exiting now") - elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED: - RNS.log("The link was closed by the server, exiting now") - else: - RNS.log("Link closed, exiting now") - - RNS.Reticulum.exit_handler() - time.sleep(1.5) - os._exit(0) + if link.teardown_reason == RNS.Link.TIMEOUT: + RNS.log("The link timed out, exiting now") + elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED: + RNS.log("The link was closed by the server, exiting now") + else: + RNS.log("Link closed, exiting now") + + RNS.Reticulum.exit_handler() + time.sleep(1.5) + os._exit(0) # When a packet is received over the link, we # simply print out the data. def client_packet_received(message, packet): - text = message.decode("utf-8") - RNS.log("Received data on the link: "+text) - print("> ", end=" ") - sys.stdout.flush() + text = message.decode("utf-8") + RNS.log("Received data on the link: "+text) + print("> ", end=" ") + sys.stdout.flush() ########################################################## @@ -212,28 +212,28 @@ def client_packet_received(message, packet): # and parses input of from the user, and then # starts up the desired program mode. if __name__ == "__main__": - try: - parser = argparse.ArgumentParser(description="Simple link example") - parser.add_argument("-s", "--server", action="store_true", help="wait for incoming link requests from clients") - parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str) - parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the server destination", type=str) - args = parser.parse_args() + try: + parser = argparse.ArgumentParser(description="Simple link example") + parser.add_argument("-s", "--server", action="store_true", help="wait for incoming link requests from clients") + parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str) + parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the server destination", type=str) + args = parser.parse_args() - if args.config: - configarg = args.config - else: - configarg = None + if args.config: + configarg = args.config + else: + configarg = None - if args.server: - server(configarg) - else: - if (args.destination == None): - print("") - parser.print_help() - print("") - else: - client(args.destination, configarg) + if args.server: + server(configarg) + else: + if (args.destination == None): + print("") + parser.print_help() + print("") + else: + client(args.destination, configarg) - except KeyboardInterrupt: - print("") - exit() \ No newline at end of file + except KeyboardInterrupt: + print("") + exit() \ No newline at end of file diff --git a/Examples/Minimal.py b/Examples/Minimal.py index a24c88e..c9cf0eb 100644 --- a/Examples/Minimal.py +++ b/Examples/Minimal.py @@ -15,45 +15,45 @@ APP_NAME = "example_utilitites" # This initialisation is executed when the program is started def program_setup(configpath): - # We must first initialise Reticulum - reticulum = RNS.Reticulum(configpath) - - # Randomly create a new identity for our example - identity = RNS.Identity() + # We must first initialise Reticulum + reticulum = RNS.Reticulum(configpath) + + # Randomly create a new identity for our example + identity = RNS.Identity() - # Using the identity we just created, we create a destination. - # Destinations are endpoints in Reticulum, that can be addressed - # and communicated with. Destinations can also announce their - # existence, which will let the network know they are reachable - # and autoomatically create paths to them, from anywhere else - # in the network. - destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "minimalsample") + # Using the identity we just created, we create a destination. + # Destinations are endpoints in Reticulum, that can be addressed + # and communicated with. Destinations can also announce their + # existence, which will let the network know they are reachable + # and autoomatically create paths to them, from anywhere else + # in the network. + destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "minimalsample") - # We configure the destination to automatically prove all - # packets adressed to it. By doing this, RNS will automatically - # generate a proof for each incoming packet and transmit it - # back to the sender of that packet. This will let anyone that - # tries to communicate with the destination know whether their - # communication was received correctly. - destination.set_proof_strategy(RNS.Destination.PROVE_ALL) - - # Everything's ready! - # Let's hand over control to the announce loop - announceLoop(destination) + # We configure the destination to automatically prove all + # packets adressed to it. By doing this, RNS will automatically + # generate a proof for each incoming packet and transmit it + # back to the sender of that packet. This will let anyone that + # tries to communicate with the destination know whether their + # communication was received correctly. + destination.set_proof_strategy(RNS.Destination.PROVE_ALL) + + # Everything's ready! + # Let's hand over control to the announce loop + announceLoop(destination) def announceLoop(destination): - # Let the user know that everything is ready - RNS.log("Minimal example "+RNS.prettyhexrep(destination.hash)+" running, hit enter to manually send an announce (Ctrl-C to quit)") + # Let the user know that everything is ready + RNS.log("Minimal example "+RNS.prettyhexrep(destination.hash)+" running, hit enter to manually send an announce (Ctrl-C to quit)") - # We enter a loop that runs until the users exits. - # If the user hits enter, we will announce our server - # destination on the network, which will let clients - # know how to create messages directed towards it. - while True: - entered = input() - destination.announce() - RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) + # We enter a loop that runs until the users exits. + # If the user hits enter, we will announce our server + # destination on the network, which will let clients + # know how to create messages directed towards it. + while True: + entered = input() + destination.announce() + RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) ########################################################## @@ -64,18 +64,18 @@ def announceLoop(destination): # and parses input from the user, and then starts # the desired program mode. if __name__ == "__main__": - try: - parser = argparse.ArgumentParser(description="Bare minimum example to start Reticulum and create a destination") - parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str) - args = parser.parse_args() + try: + parser = argparse.ArgumentParser(description="Bare minimum example to start Reticulum and create a destination") + parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str) + args = parser.parse_args() - if args.config: - configarg = args.config - else: - configarg = None + if args.config: + configarg = args.config + else: + configarg = None - program_setup(configarg) + program_setup(configarg) - except KeyboardInterrupt: - print("") - exit() \ No newline at end of file + except KeyboardInterrupt: + print("") + exit() \ No newline at end of file diff --git a/RNS/Destination.py b/RNS/Destination.py index 89a949e..7f1020e 100755 --- a/RNS/Destination.py +++ b/RNS/Destination.py @@ -10,228 +10,228 @@ from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.asymmetric import padding class Callbacks: - def __init__(self): - self.link_established = None - self.packet = None - self.proof_requested = None + def __init__(self): + self.link_established = None + self.packet = None + self.proof_requested = None class Destination: - KEYSIZE = RNS.Identity.KEYSIZE; - PADDINGSIZE= RNS.Identity.PADDINGSIZE; + KEYSIZE = RNS.Identity.KEYSIZE; + PADDINGSIZE= RNS.Identity.PADDINGSIZE; - # Constants - SINGLE = 0x00 - GROUP = 0x01 - PLAIN = 0x02 - LINK = 0x03 - types = [SINGLE, GROUP, PLAIN, LINK] + # Constants + SINGLE = 0x00 + GROUP = 0x01 + PLAIN = 0x02 + LINK = 0x03 + types = [SINGLE, GROUP, PLAIN, LINK] - PROVE_NONE = 0x21 - PROVE_APP = 0x22 - PROVE_ALL = 0x23 - proof_strategies = [PROVE_NONE, PROVE_APP, PROVE_ALL] + PROVE_NONE = 0x21 + PROVE_APP = 0x22 + PROVE_ALL = 0x23 + proof_strategies = [PROVE_NONE, PROVE_APP, PROVE_ALL] - IN = 0x11; - OUT = 0x12; - directions = [IN, OUT] + IN = 0x11; + OUT = 0x12; + directions = [IN, OUT] - @staticmethod - def getDestinationName(app_name, *aspects): - # Check input values and build name string - if "." in app_name: raise ValueError("Dots can't be used in app names") + @staticmethod + def getDestinationName(app_name, *aspects): + # Check input values and build name string + if "." in app_name: raise ValueError("Dots can't be used in app names") - name = app_name - for aspect in aspects: - if "." in aspect: raise ValueError("Dots can't be used in aspects") - name = name + "." + aspect + name = app_name + for aspect in aspects: + if "." in aspect: raise ValueError("Dots can't be used in aspects") + name = name + "." + aspect - return name + return name - @staticmethod - def getDestinationHash(app_name, *aspects): - name = Destination.getDestinationName(app_name, *aspects) + @staticmethod + def getDestinationHash(app_name, *aspects): + name = Destination.getDestinationName(app_name, *aspects) - # Create a digest for the destination - digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) - digest.update(name.encode("UTF-8")) + # Create a digest for the destination + digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) + digest.update(name.encode("UTF-8")) - return digest.finalize()[:10] + return digest.finalize()[:10] - def __init__(self, identity, direction, type, app_name, *aspects): - # Check input values and build name string - if "." in app_name: raise ValueError("Dots can't be used in app names") - if not type in Destination.types: raise ValueError("Unknown destination type") - if not direction in Destination.directions: raise ValueError("Unknown destination direction") - self.callbacks = Callbacks() - self.type = type - self.direction = direction - self.proof_strategy = Destination.PROVE_NONE - self.mtu = 0 + def __init__(self, identity, direction, type, app_name, *aspects): + # Check input values and build name string + if "." in app_name: raise ValueError("Dots can't be used in app names") + if not type in Destination.types: raise ValueError("Unknown destination type") + if not direction in Destination.directions: raise ValueError("Unknown destination direction") + self.callbacks = Callbacks() + self.type = type + self.direction = direction + self.proof_strategy = Destination.PROVE_NONE + self.mtu = 0 - self.links = [] + self.links = [] - if identity != None and type == Destination.SINGLE: - aspects = aspects+(identity.hexhash,) + if identity != None and type == Destination.SINGLE: + aspects = aspects+(identity.hexhash,) - if identity == None and direction == Destination.IN and self.type != Destination.PLAIN: - identity = RNS.Identity() - aspects = aspects+(identity.hexhash,) + if identity == None and direction == Destination.IN and self.type != Destination.PLAIN: + identity = RNS.Identity() + aspects = aspects+(identity.hexhash,) - self.identity = identity + self.identity = identity - self.name = Destination.getDestinationName(app_name, *aspects) - self.hash = Destination.getDestinationHash(app_name, *aspects) - self.hexhash = self.hash.hex() + self.name = Destination.getDestinationName(app_name, *aspects) + self.hash = Destination.getDestinationHash(app_name, *aspects) + self.hexhash = self.hash.hex() - self.callback = None - self.proofcallback = None + self.callback = None + self.proofcallback = None - RNS.Transport.registerDestination(self) + RNS.Transport.registerDestination(self) - def __str__(self): - return "<"+self.name+"/"+self.hexhash+">" + def __str__(self): + return "<"+self.name+"/"+self.hexhash+">" - def link_established_callback(self, callback): - self.callbacks.link_established = callback + def link_established_callback(self, callback): + self.callbacks.link_established = callback - def packet_callback(self, callback): - self.callbacks.packet = callback + def packet_callback(self, callback): + self.callbacks.packet = callback - def proof_requested_callback(self, callback): - self.callbacks.proof_requested = callback + def proof_requested_callback(self, callback): + self.callbacks.proof_requested = callback - def set_proof_strategy(self, proof_strategy): - if not proof_strategy in Destination.proof_strategies: - raise TypeError("Unsupported proof strategy") - else: - self.proof_strategy = proof_strategy + def set_proof_strategy(self, proof_strategy): + if not proof_strategy in Destination.proof_strategies: + raise TypeError("Unsupported proof strategy") + else: + self.proof_strategy = proof_strategy - def receive(self, packet): - plaintext = self.decrypt(packet.data) - if plaintext != None: - if packet.packet_type == RNS.Packet.LINKREQUEST: - self.incomingLinkRequest(plaintext, packet) + def receive(self, packet): + plaintext = self.decrypt(packet.data) + if plaintext != None: + if packet.packet_type == RNS.Packet.LINKREQUEST: + self.incomingLinkRequest(plaintext, packet) - if packet.packet_type == RNS.Packet.DATA: - if self.callbacks.packet != None: - self.callbacks.packet(plaintext, packet) + if packet.packet_type == RNS.Packet.DATA: + if self.callbacks.packet != None: + self.callbacks.packet(plaintext, packet) - def incomingLinkRequest(self, data, packet): - link = RNS.Link.validateRequest(self, data, packet) - if link != None: - self.links.append(link) + def incomingLinkRequest(self, data, packet): + link = RNS.Link.validateRequest(self, data, packet) + if link != None: + self.links.append(link) - def createKeys(self): - if self.type == Destination.PLAIN: - raise TypeError("A plain destination does not hold any keys") + def createKeys(self): + if self.type == Destination.PLAIN: + raise TypeError("A plain destination does not hold any keys") - if self.type == Destination.SINGLE: - raise TypeError("A single destination holds keys through an Identity instance") + if self.type == Destination.SINGLE: + raise TypeError("A single destination holds keys through an Identity instance") - if self.type == Destination.GROUP: - self.prv_bytes = Fernet.generate_key() - self.prv = Fernet(self.prv_bytes) + if self.type == Destination.GROUP: + self.prv_bytes = Fernet.generate_key() + self.prv = Fernet(self.prv_bytes) - def getPrivateKey(self): - if self.type == Destination.PLAIN: - raise TypeError("A plain destination does not hold any keys") - elif self.type == Destination.SINGLE: - raise TypeError("A single destination holds keys through an Identity instance") - else: - return self.prv_bytes + def getPrivateKey(self): + if self.type == Destination.PLAIN: + raise TypeError("A plain destination does not hold any keys") + elif self.type == Destination.SINGLE: + raise TypeError("A single destination holds keys through an Identity instance") + else: + return self.prv_bytes - def loadPrivateKey(self, key): - if self.type == Destination.PLAIN: - raise TypeError("A plain destination does not hold any keys") + def loadPrivateKey(self, key): + if self.type == Destination.PLAIN: + raise TypeError("A plain destination does not hold any keys") - if self.type == Destination.SINGLE: - raise TypeError("A single destination holds keys through an Identity instance") + if self.type == Destination.SINGLE: + raise TypeError("A single destination holds keys through an Identity instance") - if self.type == Destination.GROUP: - self.prv_bytes = key - self.prv = Fernet(self.prv_bytes) + if self.type == Destination.GROUP: + self.prv_bytes = key + self.prv = Fernet(self.prv_bytes) - def loadPublicKey(self, key): - if self.type != Destination.SINGLE: - raise TypeError("Only the \"single\" destination type can hold a public key") - else: - raise TypeError("A single destination holds keys through an Identity instance") + def loadPublicKey(self, key): + if self.type != Destination.SINGLE: + raise TypeError("Only the \"single\" destination type can hold a public key") + else: + raise TypeError("A single destination holds keys through an Identity instance") - def encrypt(self, plaintext): - if self.type == Destination.PLAIN: - return plaintext + def encrypt(self, plaintext): + if self.type == Destination.PLAIN: + return plaintext - if self.type == Destination.SINGLE and self.identity != None: - return self.identity.encrypt(plaintext) + if self.type == Destination.SINGLE and self.identity != None: + return self.identity.encrypt(plaintext) - if self.type == Destination.GROUP: - if hasattr(self, "prv") and self.prv != None: - try: - return base64.urlsafe_b64decode(self.prv.encrypt(plaintext)) - except Exception as e: - RNS.log("The GROUP destination could not encrypt data", RNS.LOG_ERROR) - RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) - else: - raise ValueError("No private key held by GROUP destination. Did you create or load one?") + if self.type == Destination.GROUP: + if hasattr(self, "prv") and self.prv != None: + try: + return base64.urlsafe_b64decode(self.prv.encrypt(plaintext)) + except Exception as e: + RNS.log("The GROUP destination could not encrypt data", RNS.LOG_ERROR) + RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) + else: + raise ValueError("No private key held by GROUP destination. Did you create or load one?") - def decrypt(self, ciphertext): - if self.type == Destination.PLAIN: - return ciphertext + def decrypt(self, ciphertext): + if self.type == Destination.PLAIN: + return ciphertext - if self.type == Destination.SINGLE and self.identity != None: - return self.identity.decrypt(ciphertext) + if self.type == Destination.SINGLE and self.identity != None: + return self.identity.decrypt(ciphertext) - if self.type == Destination.GROUP: - if hasattr(self, "prv") and self.prv != None: - try: - return self.prv.decrypt(base64.urlsafe_b64encode(ciphertext)) - except Exception as e: - RNS.log("The GROUP destination could not decrypt data", RNS.LOG_ERROR) - RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) - else: - raise ValueError("No private key held by GROUP destination. Did you create or load one?") + if self.type == Destination.GROUP: + if hasattr(self, "prv") and self.prv != None: + try: + return self.prv.decrypt(base64.urlsafe_b64encode(ciphertext)) + except Exception as e: + RNS.log("The GROUP destination could not decrypt data", RNS.LOG_ERROR) + RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) + else: + raise ValueError("No private key held by GROUP destination. Did you create or load one?") - def sign(self, message): - if self.type == Destination.SINGLE and self.identity != None: - return self.identity.sign(message) - else: - return None + def sign(self, message): + if self.type == Destination.SINGLE and self.identity != None: + return self.identity.sign(message) + else: + return None - # Creates an announce packet for this destination. - # Application specific data can be added to the announce. - def announce(self, app_data=None, path_response=False): - destination_hash = self.hash - random_hash = RNS.Identity.getRandomHash() - - signed_data = self.hash+self.identity.getPublicKey()+random_hash - if app_data != None: - signed_data += app_data + # Creates an announce packet for this destination. + # Application specific data can be added to the announce. + def announce(self, app_data=None, path_response=False): + destination_hash = self.hash + random_hash = RNS.Identity.getRandomHash() + + signed_data = self.hash+self.identity.getPublicKey()+random_hash + if app_data != None: + signed_data += app_data - signature = self.identity.sign(signed_data) + signature = self.identity.sign(signed_data) - # TODO: Check if this could be optimised by only - # carrying the hash in the destination field, not - # also redundantly inside the signed blob as here - announce_data = self.hash+self.identity.getPublicKey()+random_hash+signature + # TODO: Check if this could be optimised by only + # carrying the hash in the destination field, not + # also redundantly inside the signed blob as here + announce_data = self.hash+self.identity.getPublicKey()+random_hash+signature - if app_data != None: - announce_data += app_data + if app_data != None: + announce_data += app_data - if path_response: - announce_context = RNS.Packet.PATH_RESPONSE - else: - announce_context = RNS.Packet.NONE + if path_response: + announce_context = RNS.Packet.PATH_RESPONSE + else: + announce_context = RNS.Packet.NONE - RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context).send() + RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context).send() diff --git a/RNS/Identity.py b/RNS/Identity.py index 34b0ee9..31b5e90 100644 --- a/RNS/Identity.py +++ b/RNS/Identity.py @@ -15,313 +15,313 @@ from cryptography.hazmat.primitives.asymmetric import padding class Identity: #KEYSIZE = 1536 - KEYSIZE = 1024 - DERKEYSIZE = KEYSIZE+272 + KEYSIZE = 1024 + DERKEYSIZE = KEYSIZE+272 - # Non-configurable constants - PADDINGSIZE = 336 # In bits - HASHLENGTH = 256 # In bits - SIGLENGTH = KEYSIZE + # Non-configurable constants + PADDINGSIZE = 336 # In bits + HASHLENGTH = 256 # In bits + SIGLENGTH = KEYSIZE - ENCRYPT_CHUNKSIZE = (KEYSIZE-PADDINGSIZE)//8 - DECRYPT_CHUNKSIZE = KEYSIZE//8 + ENCRYPT_CHUNKSIZE = (KEYSIZE-PADDINGSIZE)//8 + DECRYPT_CHUNKSIZE = KEYSIZE//8 - TRUNCATED_HASHLENGTH = 80 # In bits + TRUNCATED_HASHLENGTH = 80 # In bits - # Storage - known_destinations = {} + # Storage + known_destinations = {} - @staticmethod - def remember(packet_hash, destination_hash, public_key, app_data = None): - Identity.known_destinations[destination_hash] = [time.time(), packet_hash, public_key, app_data] + @staticmethod + def remember(packet_hash, destination_hash, public_key, app_data = None): + Identity.known_destinations[destination_hash] = [time.time(), packet_hash, public_key, app_data] - @staticmethod - def recall(destination_hash): - RNS.log("Searching for "+RNS.prettyhexrep(destination_hash)+"...", RNS.LOG_EXTREME) - if destination_hash in Identity.known_destinations: - identity_data = Identity.known_destinations[destination_hash] - identity = Identity(public_only=True) - identity.loadPublicKey(identity_data[2]) - RNS.log("Found "+RNS.prettyhexrep(destination_hash)+" in known destinations", RNS.LOG_EXTREME) - return identity - else: - RNS.log("Could not find "+RNS.prettyhexrep(destination_hash)+" in known destinations", RNS.LOG_EXTREME) - return None + @staticmethod + def recall(destination_hash): + RNS.log("Searching for "+RNS.prettyhexrep(destination_hash)+"...", RNS.LOG_EXTREME) + if destination_hash in Identity.known_destinations: + identity_data = Identity.known_destinations[destination_hash] + identity = Identity(public_only=True) + identity.loadPublicKey(identity_data[2]) + RNS.log("Found "+RNS.prettyhexrep(destination_hash)+" in known destinations", RNS.LOG_EXTREME) + return identity + else: + RNS.log("Could not find "+RNS.prettyhexrep(destination_hash)+" in known destinations", RNS.LOG_EXTREME) + return None - @staticmethod - def saveKnownDestinations(): - RNS.log("Saving known destinations to storage...", RNS.LOG_VERBOSE) - file = open(RNS.Reticulum.storagepath+"/known_destinations","wb") - umsgpack.dump(Identity.known_destinations, file) - file.close() - RNS.log("Done saving known destinations to storage", RNS.LOG_VERBOSE) + @staticmethod + def saveKnownDestinations(): + RNS.log("Saving known destinations to storage...", RNS.LOG_VERBOSE) + file = open(RNS.Reticulum.storagepath+"/known_destinations","wb") + umsgpack.dump(Identity.known_destinations, file) + file.close() + RNS.log("Done saving known destinations to storage", RNS.LOG_VERBOSE) - @staticmethod - def loadKnownDestinations(): - if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"): - try: - file = open(RNS.Reticulum.storagepath+"/known_destinations","rb") - Identity.known_destinations = umsgpack.load(file) - file.close() - RNS.log("Loaded "+str(len(Identity.known_destinations))+" known destination from storage", RNS.LOG_VERBOSE) - except: - RNS.log("Error loading known destinations from disk, file will be recreated on exit", RNS.LOG_ERROR) - else: - RNS.log("Destinations file does not exist, so no known destinations loaded", RNS.LOG_VERBOSE) + @staticmethod + def loadKnownDestinations(): + if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"): + try: + file = open(RNS.Reticulum.storagepath+"/known_destinations","rb") + Identity.known_destinations = umsgpack.load(file) + file.close() + RNS.log("Loaded "+str(len(Identity.known_destinations))+" known destination from storage", RNS.LOG_VERBOSE) + except: + RNS.log("Error loading known destinations from disk, file will be recreated on exit", RNS.LOG_ERROR) + else: + RNS.log("Destinations file does not exist, so no known destinations loaded", RNS.LOG_VERBOSE) - @staticmethod - def fullHash(data): - digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) - digest.update(data) + @staticmethod + def fullHash(data): + digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) + digest.update(data) - return digest.finalize() + return digest.finalize() - @staticmethod - def truncatedHash(data): - # TODO: Remove - # digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) - # digest.update(data) + @staticmethod + def truncatedHash(data): + # TODO: Remove + # digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) + # digest.update(data) - return Identity.fullHash(data)[:(Identity.TRUNCATED_HASHLENGTH//8)] + return Identity.fullHash(data)[:(Identity.TRUNCATED_HASHLENGTH//8)] - @staticmethod - def getRandomHash(): - return Identity.truncatedHash(os.urandom(10)) + @staticmethod + def getRandomHash(): + return Identity.truncatedHash(os.urandom(10)) - @staticmethod - def validateAnnounce(packet): - if packet.packet_type == RNS.Packet.ANNOUNCE: - RNS.log("Validating announce from "+RNS.prettyhexrep(packet.destination_hash), RNS.LOG_DEBUG) - destination_hash = packet.destination_hash - public_key = packet.data[10:Identity.DERKEYSIZE//8+10] - random_hash = packet.data[Identity.DERKEYSIZE//8+10:Identity.DERKEYSIZE//8+20] - signature = packet.data[Identity.DERKEYSIZE//8+20:Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8] - app_data = b"" - if len(packet.data) > Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8: - app_data = packet.data[Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8:] + @staticmethod + def validateAnnounce(packet): + if packet.packet_type == RNS.Packet.ANNOUNCE: + RNS.log("Validating announce from "+RNS.prettyhexrep(packet.destination_hash), RNS.LOG_DEBUG) + destination_hash = packet.destination_hash + public_key = packet.data[10:Identity.DERKEYSIZE//8+10] + random_hash = packet.data[Identity.DERKEYSIZE//8+10:Identity.DERKEYSIZE//8+20] + signature = packet.data[Identity.DERKEYSIZE//8+20:Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8] + app_data = b"" + if len(packet.data) > Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8: + app_data = packet.data[Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8:] - signed_data = destination_hash+public_key+random_hash+app_data + signed_data = destination_hash+public_key+random_hash+app_data - announced_identity = Identity(public_only=True) - announced_identity.loadPublicKey(public_key) + announced_identity = Identity(public_only=True) + announced_identity.loadPublicKey(public_key) - if announced_identity.pub != None and announced_identity.validate(signature, signed_data): - RNS.Identity.remember(packet.getHash(), destination_hash, public_key) - RNS.log("Stored valid announce from "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG) - del announced_identity - return True - else: - RNS.log("Received invalid announce", RNS.LOG_DEBUG) - del announced_identity - return False + if announced_identity.pub != None and announced_identity.validate(signature, signed_data): + RNS.Identity.remember(packet.getHash(), destination_hash, public_key) + RNS.log("Stored valid announce from "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG) + del announced_identity + return True + else: + RNS.log("Received invalid announce", RNS.LOG_DEBUG) + del announced_identity + return False - @staticmethod - def exitHandler(): - Identity.saveKnownDestinations() + @staticmethod + def exitHandler(): + Identity.saveKnownDestinations() - @staticmethod - def from_file(path): - identity = Identity(public_only=True) - if identity.load(path): - return identity - else: - return None + @staticmethod + def from_file(path): + identity = Identity(public_only=True) + if identity.load(path): + return identity + else: + return None - def __init__(self,public_only=False): - # Initialize keys to none - self.prv = None - self.pub = None - self.prv_bytes = None - self.pub_bytes = None - self.hash = None - self.hexhash = None + def __init__(self,public_only=False): + # Initialize keys to none + self.prv = None + self.pub = None + self.prv_bytes = None + self.pub_bytes = None + self.hash = None + self.hexhash = None - if not public_only: - self.createKeys() + if not public_only: + self.createKeys() - def createKeys(self): - self.prv = rsa.generate_private_key( - public_exponent=65337, - key_size=Identity.KEYSIZE, - backend=default_backend() - ) - self.prv_bytes = self.prv.private_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption() - ) - self.pub = self.prv.public_key() - self.pub_bytes = self.pub.public_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PublicFormat.SubjectPublicKeyInfo - ) + def createKeys(self): + self.prv = rsa.generate_private_key( + public_exponent=65337, + key_size=Identity.KEYSIZE, + backend=default_backend() + ) + self.prv_bytes = self.prv.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() + ) + self.pub = self.prv.public_key() + self.pub_bytes = self.pub.public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ) - self.updateHashes() + self.updateHashes() - RNS.log("Identity keys created for "+RNS.prettyhexrep(self.hash), RNS.LOG_VERBOSE) + RNS.log("Identity keys created for "+RNS.prettyhexrep(self.hash), RNS.LOG_VERBOSE) - def getPrivateKey(self): - return self.prv_bytes + def getPrivateKey(self): + return self.prv_bytes - def getPublicKey(self): - return self.pub_bytes + def getPublicKey(self): + return self.pub_bytes - def loadPrivateKey(self, prv_bytes): - try: - self.prv_bytes = prv_bytes - self.prv = serialization.load_der_private_key( - self.prv_bytes, - password=None, - backend=default_backend() - ) - self.pub = self.prv.public_key() - self.pub_bytes = self.pub.public_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PublicFormat.SubjectPublicKeyInfo - ) - self.updateHashes() + def loadPrivateKey(self, prv_bytes): + try: + self.prv_bytes = prv_bytes + self.prv = serialization.load_der_private_key( + self.prv_bytes, + password=None, + backend=default_backend() + ) + self.pub = self.prv.public_key() + self.pub_bytes = self.pub.public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + self.updateHashes() - return True + return True - except Exception as e: - RNS.log("Failed to load identity key", RNS.LOG_ERROR) - RNS.log("The contained exception was: "+str(e)) - return False + except Exception as e: + RNS.log("Failed to load identity key", RNS.LOG_ERROR) + RNS.log("The contained exception was: "+str(e)) + return False - def loadPublicKey(self, key): - try: - self.pub_bytes = key - self.pub = load_der_public_key(self.pub_bytes, backend=default_backend()) - self.updateHashes() - except Exception as e: - RNS.log("Error while loading public key, the contained exception was: "+str(e), RNS.LOG_ERROR) + def loadPublicKey(self, key): + try: + self.pub_bytes = key + self.pub = load_der_public_key(self.pub_bytes, backend=default_backend()) + self.updateHashes() + except Exception as e: + RNS.log("Error while loading public key, the contained exception was: "+str(e), RNS.LOG_ERROR) - def updateHashes(self): - self.hash = Identity.truncatedHash(self.pub_bytes) - self.hexhash = self.hash.hex() + def updateHashes(self): + self.hash = Identity.truncatedHash(self.pub_bytes) + self.hexhash = self.hash.hex() - def save(self, path): - try: - with open(path, "wb") as key_file: - key_file.write(self.prv_bytes) - return True - return False - except Exception as e: - RNS.log("Error while saving identity to "+str(path), RNS.LOG_ERROR) - RNS.log("The contained exception was: "+str(e)) + def save(self, path): + try: + with open(path, "wb") as key_file: + key_file.write(self.prv_bytes) + return True + return False + except Exception as e: + RNS.log("Error while saving identity to "+str(path), RNS.LOG_ERROR) + RNS.log("The contained exception was: "+str(e)) - def load(self, path): - try: - with open(path, "rb") as key_file: - prv_bytes = key_file.read() - return self.loadPrivateKey(prv_bytes) - return False - except Exception as e: - RNS.log("Error while loading identity from "+str(path), RNS.LOG_ERROR) - RNS.log("The contained exception was: "+str(e)) + def load(self, path): + try: + with open(path, "rb") as key_file: + prv_bytes = key_file.read() + return self.loadPrivateKey(prv_bytes) + return False + except Exception as e: + RNS.log("Error while loading identity from "+str(path), RNS.LOG_ERROR) + RNS.log("The contained exception was: "+str(e)) - def encrypt(self, plaintext): - if self.pub != None: - chunksize = Identity.ENCRYPT_CHUNKSIZE - chunks = int(math.ceil(len(plaintext)/(float(chunksize)))) + def encrypt(self, plaintext): + if self.pub != None: + chunksize = Identity.ENCRYPT_CHUNKSIZE + chunks = int(math.ceil(len(plaintext)/(float(chunksize)))) - ciphertext = b""; - for chunk in range(chunks): - start = chunk*chunksize - end = (chunk+1)*chunksize - if (chunk+1)*chunksize > len(plaintext): - end = len(plaintext) - - ciphertext += self.pub.encrypt( - plaintext[start:end], - padding.OAEP( - mgf=padding.MGF1(algorithm=hashes.SHA1()), - algorithm=hashes.SHA1(), - label=None - ) - ) - return ciphertext - else: - raise KeyError("Encryption failed because identity does not hold a public key") + ciphertext = b""; + for chunk in range(chunks): + start = chunk*chunksize + end = (chunk+1)*chunksize + if (chunk+1)*chunksize > len(plaintext): + end = len(plaintext) + + ciphertext += self.pub.encrypt( + plaintext[start:end], + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA1()), + algorithm=hashes.SHA1(), + label=None + ) + ) + return ciphertext + else: + raise KeyError("Encryption failed because identity does not hold a public key") - def decrypt(self, ciphertext): - if self.prv != None: - plaintext = None - try: - chunksize = Identity.DECRYPT_CHUNKSIZE - chunks = int(math.ceil(len(ciphertext)/(float(chunksize)))) + def decrypt(self, ciphertext): + if self.prv != None: + plaintext = None + try: + chunksize = Identity.DECRYPT_CHUNKSIZE + chunks = int(math.ceil(len(ciphertext)/(float(chunksize)))) - plaintext = b""; - for chunk in range(chunks): - start = chunk*chunksize - end = (chunk+1)*chunksize - if (chunk+1)*chunksize > len(ciphertext): - end = len(ciphertext) + plaintext = b""; + for chunk in range(chunks): + start = chunk*chunksize + end = (chunk+1)*chunksize + if (chunk+1)*chunksize > len(ciphertext): + end = len(ciphertext) - plaintext += self.prv.decrypt( - ciphertext[start:end], - padding.OAEP( - mgf=padding.MGF1(algorithm=hashes.SHA1()), - algorithm=hashes.SHA1(), - label=None - ) - ) - except: - RNS.log("Decryption by "+RNS.prettyhexrep(self.hash)+" failed", RNS.LOG_VERBOSE) - - return plaintext; - else: - raise KeyError("Decryption failed because identity does not hold a private key") + plaintext += self.prv.decrypt( + ciphertext[start:end], + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA1()), + algorithm=hashes.SHA1(), + label=None + ) + ) + except: + RNS.log("Decryption by "+RNS.prettyhexrep(self.hash)+" failed", RNS.LOG_VERBOSE) + + return plaintext; + else: + raise KeyError("Decryption failed because identity does not hold a private key") - def sign(self, message): - if self.prv != None: - signature = self.prv.sign( - message, - padding.PSS( - mgf=padding.MGF1(hashes.SHA256()), - salt_length=padding.PSS.MAX_LENGTH - ), - hashes.SHA256() - ) - return signature - else: - raise KeyError("Signing failed because identity does not hold a private key") + def sign(self, message): + if self.prv != None: + signature = self.prv.sign( + message, + padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), + salt_length=padding.PSS.MAX_LENGTH + ), + hashes.SHA256() + ) + return signature + else: + raise KeyError("Signing failed because identity does not hold a private key") - def validate(self, signature, message): - if self.pub != None: - try: - self.pub.verify( - signature, - message, - padding.PSS( - mgf=padding.MGF1(hashes.SHA256()), - salt_length=padding.PSS.MAX_LENGTH - ), - hashes.SHA256() - ) - return True - except Exception as e: - return False - else: - raise KeyError("Signature validation failed because identity does not hold a public key") + def validate(self, signature, message): + if self.pub != None: + try: + self.pub.verify( + signature, + message, + padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), + salt_length=padding.PSS.MAX_LENGTH + ), + hashes.SHA256() + ) + return True + except Exception as e: + return False + else: + raise KeyError("Signature validation failed because identity does not hold a public key") - def prove(self, packet, destination=None): - signature = self.sign(packet.packet_hash) - if RNS.Reticulum.should_use_implicit_proof(): - proof_data = signature - else: - proof_data = packet.packet_hash + signature - - if destination == None: - destination = packet.generateProofDestination() + def prove(self, packet, destination=None): + signature = self.sign(packet.packet_hash) + if RNS.Reticulum.should_use_implicit_proof(): + proof_data = signature + else: + proof_data = packet.packet_hash + signature + + if destination == None: + destination = packet.generateProofDestination() - proof = RNS.Packet(destination, proof_data, RNS.Packet.PROOF, attached_interface = packet.receiving_interface) - proof.send() + proof = RNS.Packet(destination, proof_data, RNS.Packet.PROOF, attached_interface = packet.receiving_interface) + proof.send() - def __str__(self): - return RNS.prettyhexrep(self.hash) + def __str__(self): + return RNS.prettyhexrep(self.hash) diff --git a/RNS/Interfaces/AX25KISSInterface.py b/RNS/Interfaces/AX25KISSInterface.py index d699549..820e258 100644 --- a/RNS/Interfaces/AX25KISSInterface.py +++ b/RNS/Interfaces/AX25KISSInterface.py @@ -8,298 +8,298 @@ import time import RNS class KISS(): - FEND = 0xC0 - FESC = 0xDB - TFEND = 0xDC - TFESC = 0xDD - CMD_UNKNOWN = 0xFE - CMD_DATA = 0x00 - CMD_TXDELAY = 0x01 - CMD_P = 0x02 - CMD_SLOTTIME = 0x03 - CMD_TXTAIL = 0x04 - CMD_FULLDUPLEX = 0x05 - CMD_SETHARDWARE = 0x06 - CMD_READY = 0x0F - CMD_RETURN = 0xFF + FEND = 0xC0 + FESC = 0xDB + TFEND = 0xDC + TFESC = 0xDD + CMD_UNKNOWN = 0xFE + CMD_DATA = 0x00 + CMD_TXDELAY = 0x01 + CMD_P = 0x02 + CMD_SLOTTIME = 0x03 + CMD_TXTAIL = 0x04 + CMD_FULLDUPLEX = 0x05 + CMD_SETHARDWARE = 0x06 + CMD_READY = 0x0F + CMD_RETURN = 0xFF - @staticmethod - def escape(data): - data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd])) - data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc])) - return data + @staticmethod + def escape(data): + data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd])) + data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc])) + return data class AX25(): - PID_NOLAYER3 = 0xF0 - CTRL_UI = 0x03 - CRC_CORRECT = bytes([0xF0])+bytes([0xB8]) - HEADER_SIZE = 16 + PID_NOLAYER3 = 0xF0 + CTRL_UI = 0x03 + CRC_CORRECT = bytes([0xF0])+bytes([0xB8]) + HEADER_SIZE = 16 class AX25KISSInterface(Interface): - MAX_CHUNK = 32768 + MAX_CHUNK = 32768 - owner = None - port = None - speed = None - databits = None - parity = None - stopbits = None - serial = None + owner = None + port = None + speed = None + databits = None + parity = None + stopbits = None + serial = None - def __init__(self, owner, name, callsign, ssid, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control): - self.serial = None - self.owner = owner - self.name = name - self.src_call = callsign.upper().encode("ascii") - self.src_ssid = ssid - self.dst_call = "APZRNS".encode("ascii") - self.dst_ssid = 0 - self.port = port - self.speed = speed - self.databits = databits - self.parity = serial.PARITY_NONE - self.stopbits = stopbits - self.timeout = 100 - self.online = False - # TODO: Sane default and make this configurable - # TODO: Changed to 25ms instead of 100ms, check it - self.txdelay = 0.025 + def __init__(self, owner, name, callsign, ssid, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control): + self.serial = None + self.owner = owner + self.name = name + self.src_call = callsign.upper().encode("ascii") + self.src_ssid = ssid + self.dst_call = "APZRNS".encode("ascii") + self.dst_ssid = 0 + self.port = port + self.speed = speed + self.databits = databits + self.parity = serial.PARITY_NONE + self.stopbits = stopbits + self.timeout = 100 + self.online = False + # TODO: Sane default and make this configurable + # TODO: Changed to 25ms instead of 100ms, check it + self.txdelay = 0.025 - self.packet_queue = [] - self.flow_control = flow_control - self.interface_ready = False + self.packet_queue = [] + self.flow_control = flow_control + self.interface_ready = False - if (len(self.src_call) < 3 or len(self.src_call) > 6): - raise ValueError("Invalid callsign for "+str(self)) + if (len(self.src_call) < 3 or len(self.src_call) > 6): + raise ValueError("Invalid callsign for "+str(self)) - if (self.src_ssid < 0 or self.src_ssid > 15): - raise ValueError("Invalid SSID for "+str(self)) + if (self.src_ssid < 0 or self.src_ssid > 15): + raise ValueError("Invalid SSID for "+str(self)) - self.preamble = preamble if preamble != None else 350; - self.txtail = txtail if txtail != None else 20; - self.persistence = persistence if persistence != None else 64; - self.slottime = slottime if slottime != None else 20; + self.preamble = preamble if preamble != None else 350; + self.txtail = txtail if txtail != None else 20; + self.persistence = persistence if persistence != None else 64; + self.slottime = slottime if slottime != None else 20; - if parity.lower() == "e" or parity.lower() == "even": - self.parity = serial.PARITY_EVEN + if parity.lower() == "e" or parity.lower() == "even": + self.parity = serial.PARITY_EVEN - if parity.lower() == "o" or parity.lower() == "odd": - self.parity = serial.PARITY_ODD + if parity.lower() == "o" or parity.lower() == "odd": + self.parity = serial.PARITY_ODD - try: - RNS.log("Opening serial port "+self.port+"...") - self.serial = serial.Serial( - port = self.port, - baudrate = self.speed, - bytesize = self.databits, - parity = self.parity, - stopbits = self.stopbits, - xonxoff = False, - rtscts = False, - timeout = 0, - inter_byte_timeout = None, - write_timeout = None, - dsrdtr = False, - ) - except Exception as e: - RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR) - raise e + try: + RNS.log("Opening serial port "+self.port+"...") + self.serial = serial.Serial( + port = self.port, + baudrate = self.speed, + bytesize = self.databits, + parity = self.parity, + stopbits = self.stopbits, + xonxoff = False, + rtscts = False, + timeout = 0, + inter_byte_timeout = None, + write_timeout = None, + dsrdtr = False, + ) + except Exception as e: + RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR) + raise e - if self.serial.is_open: - # Allow time for interface to initialise before config - sleep(2.0) - thread = threading.Thread(target=self.readLoop) - thread.setDaemon(True) - thread.start() - self.online = True - RNS.log("Serial port "+self.port+" is now open") - RNS.log("Configuring AX.25 KISS interface parameters...") - self.setPreamble(self.preamble) - self.setTxTail(self.txtail) - self.setPersistence(self.persistence) - self.setSlotTime(self.slottime) - self.setFlowControl(self.flow_control) - self.interface_ready = True - RNS.log("AX.25 KISS interface configured") - sleep(2) - else: - raise IOError("Could not open serial port") + if self.serial.is_open: + # Allow time for interface to initialise before config + sleep(2.0) + thread = threading.Thread(target=self.readLoop) + thread.setDaemon(True) + thread.start() + self.online = True + RNS.log("Serial port "+self.port+" is now open") + RNS.log("Configuring AX.25 KISS interface parameters...") + self.setPreamble(self.preamble) + self.setTxTail(self.txtail) + self.setPersistence(self.persistence) + self.setSlotTime(self.slottime) + self.setFlowControl(self.flow_control) + self.interface_ready = True + RNS.log("AX.25 KISS interface configured") + sleep(2) + else: + raise IOError("Could not open serial port") - def setPreamble(self, preamble): - preamble_ms = preamble - preamble = int(preamble_ms / 10) - if preamble < 0: - preamble = 0 - if preamble > 255: - preamble = 255 + def setPreamble(self, preamble): + preamble_ms = preamble + preamble = int(preamble_ms / 10) + if preamble < 0: + preamble = 0 + if preamble > 255: + preamble = 255 - kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXDELAY])+bytes([preamble])+bytes([KISS.FEND]) - written = self.serial.write(kiss_command) - if written != len(kiss_command): - raise IOError("Could not configure AX.25 KISS interface preamble to "+str(preamble_ms)+" (command value "+str(preamble)+")") + kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXDELAY])+bytes([preamble])+bytes([KISS.FEND]) + written = self.serial.write(kiss_command) + if written != len(kiss_command): + raise IOError("Could not configure AX.25 KISS interface preamble to "+str(preamble_ms)+" (command value "+str(preamble)+")") - def setTxTail(self, txtail): - txtail_ms = txtail - txtail = int(txtail_ms / 10) - if txtail < 0: - txtail = 0 - if txtail > 255: - txtail = 255 + def setTxTail(self, txtail): + txtail_ms = txtail + txtail = int(txtail_ms / 10) + if txtail < 0: + txtail = 0 + if txtail > 255: + txtail = 255 - kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXTAIL])+bytes([txtail])+bytes([KISS.FEND]) - written = self.serial.write(kiss_command) - if written != len(kiss_command): - raise IOError("Could not configure AX.25 KISS interface TX tail to "+str(txtail_ms)+" (command value "+str(txtail)+")") + kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXTAIL])+bytes([txtail])+bytes([KISS.FEND]) + written = self.serial.write(kiss_command) + if written != len(kiss_command): + raise IOError("Could not configure AX.25 KISS interface TX tail to "+str(txtail_ms)+" (command value "+str(txtail)+")") - def setPersistence(self, persistence): - if persistence < 0: - persistence = 0 - if persistence > 255: - persistence = 255 + def setPersistence(self, persistence): + if persistence < 0: + persistence = 0 + if persistence > 255: + persistence = 255 - kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_P])+bytes([persistence])+bytes([KISS.FEND]) - written = self.serial.write(kiss_command) - if written != len(kiss_command): - raise IOError("Could not configure AX.25 KISS interface persistence to "+str(persistence)) + kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_P])+bytes([persistence])+bytes([KISS.FEND]) + written = self.serial.write(kiss_command) + if written != len(kiss_command): + raise IOError("Could not configure AX.25 KISS interface persistence to "+str(persistence)) - def setSlotTime(self, slottime): - slottime_ms = slottime - slottime = int(slottime_ms / 10) - if slottime < 0: - slottime = 0 - if slottime > 255: - slottime = 255 + def setSlotTime(self, slottime): + slottime_ms = slottime + slottime = int(slottime_ms / 10) + if slottime < 0: + slottime = 0 + if slottime > 255: + slottime = 255 - kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SLOTTIME])+bytes([slottime])+bytes([KISS.FEND]) - written = self.serial.write(kiss_command) - if written != len(kiss_command): - raise IOError("Could not configure AX.25 KISS interface slot time to "+str(slottime_ms)+" (command value "+str(slottime)+")") + kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SLOTTIME])+bytes([slottime])+bytes([KISS.FEND]) + written = self.serial.write(kiss_command) + if written != len(kiss_command): + raise IOError("Could not configure AX.25 KISS interface slot time to "+str(slottime_ms)+" (command value "+str(slottime)+")") - def setFlowControl(self, flow_control): - kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_READY])+bytes([0x01])+bytes([KISS.FEND]) - written = self.serial.write(kiss_command) - if written != len(kiss_command): - if (flow_control): - raise IOError("Could not enable AX.25 KISS interface flow control") - else: - raise IOError("Could not enable AX.25 KISS interface flow control") + def setFlowControl(self, flow_control): + kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_READY])+bytes([0x01])+bytes([KISS.FEND]) + written = self.serial.write(kiss_command) + if written != len(kiss_command): + if (flow_control): + raise IOError("Could not enable AX.25 KISS interface flow control") + else: + raise IOError("Could not enable AX.25 KISS interface flow control") - def processIncoming(self, data): - if (len(data) > AX25.HEADER_SIZE): - self.owner.inbound(data[AX25.HEADER_SIZE:], self) + def processIncoming(self, data): + if (len(data) > AX25.HEADER_SIZE): + self.owner.inbound(data[AX25.HEADER_SIZE:], self) - def processOutgoing(self,data): - if self.online: - if self.interface_ready: - if self.flow_control: - self.interface_ready = False + def processOutgoing(self,data): + if self.online: + if self.interface_ready: + if self.flow_control: + self.interface_ready = False - encoded_dst_ssid = bytes([0x60 | (self.dst_ssid << 1)]) - encoded_src_ssid = bytes([0x60 | (self.src_ssid << 1) | 0x01]) + encoded_dst_ssid = bytes([0x60 | (self.dst_ssid << 1)]) + encoded_src_ssid = bytes([0x60 | (self.src_ssid << 1) | 0x01]) - addr = b"" + addr = b"" - for i in range(0,6): - if (i < len(self.dst_call)): - addr += bytes([self.dst_call[i]<<1]) - else: - addr += bytes([0x20]) - addr += encoded_dst_ssid + for i in range(0,6): + if (i < len(self.dst_call)): + addr += bytes([self.dst_call[i]<<1]) + else: + addr += bytes([0x20]) + addr += encoded_dst_ssid - for i in range(0,6): - if (i < len(self.src_call)): - addr += bytes([self.src_call[i]<<1]) - else: - addr += bytes([0x20]) - addr += encoded_src_ssid + for i in range(0,6): + if (i < len(self.src_call)): + addr += bytes([self.src_call[i]<<1]) + else: + addr += bytes([0x20]) + addr += encoded_src_ssid - data = addr+bytes([AX25.CTRL_UI])+bytes([AX25.PID_NOLAYER3])+data + data = addr+bytes([AX25.CTRL_UI])+bytes([AX25.PID_NOLAYER3])+data - data = data.replace(bytes([0xdb]), bytes([0xdb])+bytes([0xdd])) - data = data.replace(bytes([0xc0]), bytes([0xdb])+bytes([0xdc])) - kiss_frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND]) + data = data.replace(bytes([0xdb]), bytes([0xdb])+bytes([0xdd])) + data = data.replace(bytes([0xc0]), bytes([0xdb])+bytes([0xdc])) + kiss_frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND]) - if (self.txdelay > 0): - RNS.log(str(self.name)+" delaying TX for "+str(self.txdelay)+" seconds", RNS.LOG_EXTREME) - sleep(self.txdelay) + if (self.txdelay > 0): + RNS.log(str(self.name)+" delaying TX for "+str(self.txdelay)+" seconds", RNS.LOG_EXTREME) + sleep(self.txdelay) - written = self.serial.write(kiss_frame) - if written != len(kiss_frame): - if self.flow_control: - self.interface_ready = True - raise IOError("AX.25 interface only wrote "+str(written)+" bytes of "+str(len(kiss_frame))) - else: - self.queue(data) + written = self.serial.write(kiss_frame) + if written != len(kiss_frame): + if self.flow_control: + self.interface_ready = True + raise IOError("AX.25 interface only wrote "+str(written)+" bytes of "+str(len(kiss_frame))) + else: + self.queue(data) - def queue(self, data): - self.packet_queue.append(data) + def queue(self, data): + self.packet_queue.append(data) - def process_queue(self): - if len(self.packet_queue) > 0: - data = self.packet_queue.pop(0) - self.interface_ready = True - self.processOutgoing(data) - elif len(self.packet_queue) == 0: - self.interface_ready = True + def process_queue(self): + if len(self.packet_queue) > 0: + data = self.packet_queue.pop(0) + self.interface_ready = True + self.processOutgoing(data) + elif len(self.packet_queue) == 0: + self.interface_ready = True - def readLoop(self): - try: - in_frame = False - escape = False - command = KISS.CMD_UNKNOWN - data_buffer = b"" - last_read_ms = int(time.time()*1000) + def readLoop(self): + try: + in_frame = False + escape = False + command = KISS.CMD_UNKNOWN + data_buffer = b"" + last_read_ms = int(time.time()*1000) - while self.serial.is_open: - if self.serial.in_waiting: - byte = ord(self.serial.read(1)) - last_read_ms = int(time.time()*1000) + while self.serial.is_open: + if self.serial.in_waiting: + byte = ord(self.serial.read(1)) + last_read_ms = int(time.time()*1000) - if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA): - in_frame = False - self.processIncoming(data_buffer) - elif (byte == KISS.FEND): - in_frame = True - command = KISS.CMD_UNKNOWN - data_buffer = b"" - elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU+AX25.HEADER_SIZE): - if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN): - # We only support one HDLC port for now, so - # strip off the port nibble - byte = byte & 0x0F - command = byte - elif (command == KISS.CMD_DATA): - if (byte == KISS.FESC): - escape = True - else: - if (escape): - if (byte == KISS.TFEND): - byte = KISS.FEND - if (byte == KISS.TFESC): - byte = KISS.FESC - escape = False - data_buffer = data_buffer+bytes([byte]) - elif (command == KISS.CMD_READY): - # TODO: add timeout and reset if ready - # command never arrives - self.process_queue() - else: - time_since_last = int(time.time()*1000) - last_read_ms - if len(data_buffer) > 0 and time_since_last > self.timeout: - data_buffer = b"" - in_frame = False - command = KISS.CMD_UNKNOWN - escape = False - sleep(0.08) + if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA): + in_frame = False + self.processIncoming(data_buffer) + elif (byte == KISS.FEND): + in_frame = True + command = KISS.CMD_UNKNOWN + data_buffer = b"" + elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU+AX25.HEADER_SIZE): + if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN): + # We only support one HDLC port for now, so + # strip off the port nibble + byte = byte & 0x0F + command = byte + elif (command == KISS.CMD_DATA): + if (byte == KISS.FESC): + escape = True + else: + if (escape): + if (byte == KISS.TFEND): + byte = KISS.FEND + if (byte == KISS.TFESC): + byte = KISS.FESC + escape = False + data_buffer = data_buffer+bytes([byte]) + elif (command == KISS.CMD_READY): + # TODO: add timeout and reset if ready + # command never arrives + self.process_queue() + else: + time_since_last = int(time.time()*1000) - last_read_ms + if len(data_buffer) > 0 and time_since_last > self.timeout: + data_buffer = b"" + in_frame = False + command = KISS.CMD_UNKNOWN + escape = False + sleep(0.08) - except Exception as e: - self.online = False - RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) - RNS.log("The interface "+str(self.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR) + except Exception as e: + self.online = False + RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) + RNS.log("The interface "+str(self.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR) - def __str__(self): - return "AX25KISSInterface["+self.name+"]" \ No newline at end of file + def __str__(self): + return "AX25KISSInterface["+self.name+"]" \ No newline at end of file diff --git a/RNS/Interfaces/Interface.py b/RNS/Interfaces/Interface.py index a04462b..8b82f43 100755 --- a/RNS/Interfaces/Interface.py +++ b/RNS/Interfaces/Interface.py @@ -11,5 +11,5 @@ class Interface: pass def get_hash(self): - # TODO: Maybe expand this to something more unique - return RNS.Identity.fullHash(str(self).encode("utf-8")) \ No newline at end of file + # TODO: Maybe expand this to something more unique + return RNS.Identity.fullHash(str(self).encode("utf-8")) \ No newline at end of file diff --git a/RNS/Interfaces/KISSInterface.py b/RNS/Interfaces/KISSInterface.py index e6b9609..4661e03 100644 --- a/RNS/Interfaces/KISSInterface.py +++ b/RNS/Interfaces/KISSInterface.py @@ -7,250 +7,250 @@ import time import RNS class KISS(): - FEND = 0xC0 - FESC = 0xDB - TFEND = 0xDC - TFESC = 0xDD - CMD_UNKNOWN = 0xFE - CMD_DATA = 0x00 - CMD_TXDELAY = 0x01 - CMD_P = 0x02 - CMD_SLOTTIME = 0x03 - CMD_TXTAIL = 0x04 - CMD_FULLDUPLEX = 0x05 - CMD_SETHARDWARE = 0x06 - CMD_READY = 0x0F - CMD_RETURN = 0xFF + FEND = 0xC0 + FESC = 0xDB + TFEND = 0xDC + TFESC = 0xDD + CMD_UNKNOWN = 0xFE + CMD_DATA = 0x00 + CMD_TXDELAY = 0x01 + CMD_P = 0x02 + CMD_SLOTTIME = 0x03 + CMD_TXTAIL = 0x04 + CMD_FULLDUPLEX = 0x05 + CMD_SETHARDWARE = 0x06 + CMD_READY = 0x0F + CMD_RETURN = 0xFF - @staticmethod - def escape(data): - data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd])) - data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc])) - return data + @staticmethod + def escape(data): + data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd])) + data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc])) + return data class KISSInterface(Interface): - MAX_CHUNK = 32768 + MAX_CHUNK = 32768 - owner = None - port = None - speed = None - databits = None - parity = None - stopbits = None - serial = None + owner = None + port = None + speed = None + databits = None + parity = None + stopbits = None + serial = None - def __init__(self, owner, name, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control): - self.serial = None - self.owner = owner - self.name = name - self.port = port - self.speed = speed - self.databits = databits - self.parity = serial.PARITY_NONE - self.stopbits = stopbits - self.timeout = 100 - self.online = False + def __init__(self, owner, name, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control): + self.serial = None + self.owner = owner + self.name = name + self.port = port + self.speed = speed + self.databits = databits + self.parity = serial.PARITY_NONE + self.stopbits = stopbits + self.timeout = 100 + self.online = False - self.packet_queue = [] - self.flow_control = flow_control - self.interface_ready = False + self.packet_queue = [] + self.flow_control = flow_control + self.interface_ready = False - self.preamble = preamble if preamble != None else 350; - self.txtail = txtail if txtail != None else 20; - self.persistence = persistence if persistence != None else 64; - self.slottime = slottime if slottime != None else 20; + self.preamble = preamble if preamble != None else 350; + self.txtail = txtail if txtail != None else 20; + self.persistence = persistence if persistence != None else 64; + self.slottime = slottime if slottime != None else 20; - if parity.lower() == "e" or parity.lower() == "even": - self.parity = serial.PARITY_EVEN + if parity.lower() == "e" or parity.lower() == "even": + self.parity = serial.PARITY_EVEN - if parity.lower() == "o" or parity.lower() == "odd": - self.parity = serial.PARITY_ODD + if parity.lower() == "o" or parity.lower() == "odd": + self.parity = serial.PARITY_ODD - try: - RNS.log("Opening serial port "+self.port+"...") - self.serial = serial.Serial( - port = self.port, - baudrate = self.speed, - bytesize = self.databits, - parity = self.parity, - stopbits = self.stopbits, - xonxoff = False, - rtscts = False, - timeout = 0, - inter_byte_timeout = None, - write_timeout = None, - dsrdtr = False, - ) - except Exception as e: - RNS.log("Could not open serial port "+self.port, RNS.LOG_ERROR) - raise e + try: + RNS.log("Opening serial port "+self.port+"...") + self.serial = serial.Serial( + port = self.port, + baudrate = self.speed, + bytesize = self.databits, + parity = self.parity, + stopbits = self.stopbits, + xonxoff = False, + rtscts = False, + timeout = 0, + inter_byte_timeout = None, + write_timeout = None, + dsrdtr = False, + ) + except Exception as e: + RNS.log("Could not open serial port "+self.port, RNS.LOG_ERROR) + raise e - if self.serial.is_open: - # Allow time for interface to initialise before config - sleep(2.0) - thread = threading.Thread(target=self.readLoop) - thread.setDaemon(True) - thread.start() - self.online = True - RNS.log("Serial port "+self.port+" is now open") - RNS.log("Configuring KISS interface parameters...") - self.setPreamble(self.preamble) - self.setTxTail(self.txtail) - self.setPersistence(self.persistence) - self.setSlotTime(self.slottime) - self.setFlowControl(self.flow_control) - self.interface_ready = True - RNS.log("KISS interface configured") - else: - raise IOError("Could not open serial port") + if self.serial.is_open: + # Allow time for interface to initialise before config + sleep(2.0) + thread = threading.Thread(target=self.readLoop) + thread.setDaemon(True) + thread.start() + self.online = True + RNS.log("Serial port "+self.port+" is now open") + RNS.log("Configuring KISS interface parameters...") + self.setPreamble(self.preamble) + self.setTxTail(self.txtail) + self.setPersistence(self.persistence) + self.setSlotTime(self.slottime) + self.setFlowControl(self.flow_control) + self.interface_ready = True + RNS.log("KISS interface configured") + else: + raise IOError("Could not open serial port") - def setPreamble(self, preamble): - preamble_ms = preamble - preamble = int(preamble_ms / 10) - if preamble < 0: - preamble = 0 - if preamble > 255: - preamble = 255 + def setPreamble(self, preamble): + preamble_ms = preamble + preamble = int(preamble_ms / 10) + if preamble < 0: + preamble = 0 + if preamble > 255: + preamble = 255 - kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXDELAY])+bytes([preamble])+bytes([KISS.FEND]) - written = self.serial.write(kiss_command) - if written != len(kiss_command): - raise IOError("Could not configure KISS interface preamble to "+str(preamble_ms)+" (command value "+str(preamble)+")") + kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXDELAY])+bytes([preamble])+bytes([KISS.FEND]) + written = self.serial.write(kiss_command) + if written != len(kiss_command): + raise IOError("Could not configure KISS interface preamble to "+str(preamble_ms)+" (command value "+str(preamble)+")") - def setTxTail(self, txtail): - txtail_ms = txtail - txtail = int(txtail_ms / 10) - if txtail < 0: - txtail = 0 - if txtail > 255: - txtail = 255 + def setTxTail(self, txtail): + txtail_ms = txtail + txtail = int(txtail_ms / 10) + if txtail < 0: + txtail = 0 + if txtail > 255: + txtail = 255 - kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXTAIL])+bytes([txtail])+bytes([KISS.FEND]) - written = self.serial.write(kiss_command) - if written != len(kiss_command): - raise IOError("Could not configure KISS interface TX tail to "+str(txtail_ms)+" (command value "+str(txtail)+")") + kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXTAIL])+bytes([txtail])+bytes([KISS.FEND]) + written = self.serial.write(kiss_command) + if written != len(kiss_command): + raise IOError("Could not configure KISS interface TX tail to "+str(txtail_ms)+" (command value "+str(txtail)+")") - def setPersistence(self, persistence): - if persistence < 0: - persistence = 0 - if persistence > 255: - persistence = 255 + def setPersistence(self, persistence): + if persistence < 0: + persistence = 0 + if persistence > 255: + persistence = 255 - kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_P])+bytes([persistence])+bytes([KISS.FEND]) - written = self.serial.write(kiss_command) - if written != len(kiss_command): - raise IOError("Could not configure KISS interface persistence to "+str(persistence)) + kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_P])+bytes([persistence])+bytes([KISS.FEND]) + written = self.serial.write(kiss_command) + if written != len(kiss_command): + raise IOError("Could not configure KISS interface persistence to "+str(persistence)) - def setSlotTime(self, slottime): - slottime_ms = slottime - slottime = int(slottime_ms / 10) - if slottime < 0: - slottime = 0 - if slottime > 255: - slottime = 255 + def setSlotTime(self, slottime): + slottime_ms = slottime + slottime = int(slottime_ms / 10) + if slottime < 0: + slottime = 0 + if slottime > 255: + slottime = 255 - kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SLOTTIME])+bytes([slottime])+bytes([KISS.FEND]) - written = self.serial.write(kiss_command) - if written != len(kiss_command): - raise IOError("Could not configure KISS interface slot time to "+str(slottime_ms)+" (command value "+str(slottime)+")") + kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SLOTTIME])+bytes([slottime])+bytes([KISS.FEND]) + written = self.serial.write(kiss_command) + if written != len(kiss_command): + raise IOError("Could not configure KISS interface slot time to "+str(slottime_ms)+" (command value "+str(slottime)+")") - def setFlowControl(self, flow_control): - kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_READY])+bytes([0x01])+bytes([KISS.FEND]) - written = self.serial.write(kiss_command) - if written != len(kiss_command): - if (flow_control): - raise IOError("Could not enable KISS interface flow control") - else: - raise IOError("Could not enable KISS interface flow control") + def setFlowControl(self, flow_control): + kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_READY])+bytes([0x01])+bytes([KISS.FEND]) + written = self.serial.write(kiss_command) + if written != len(kiss_command): + if (flow_control): + raise IOError("Could not enable KISS interface flow control") + else: + raise IOError("Could not enable KISS interface flow control") - def processIncoming(self, data): - self.owner.inbound(data, self) + def processIncoming(self, data): + self.owner.inbound(data, self) - def processOutgoing(self,data): - if self.online: - if self.interface_ready: - if self.flow_control: - self.interface_ready = False + def processOutgoing(self,data): + if self.online: + if self.interface_ready: + if self.flow_control: + self.interface_ready = False - data = data.replace(bytes([0xdb]), bytes([0xdb])+bytes([0xdd])) - data = data.replace(bytes([0xc0]), bytes([0xdb])+bytes([0xdc])) - frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND]) + data = data.replace(bytes([0xdb]), bytes([0xdb])+bytes([0xdd])) + data = data.replace(bytes([0xc0]), bytes([0xdb])+bytes([0xdc])) + frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND]) - written = self.serial.write(frame) - if written != len(frame): - raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data))) + written = self.serial.write(frame) + if written != len(frame): + raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data))) - else: - self.queue(data) + else: + self.queue(data) - def queue(self, data): - self.packet_queue.append(data) + def queue(self, data): + self.packet_queue.append(data) - def process_queue(self): - if len(self.packet_queue) > 0: - data = self.packet_queue.pop(0) - self.interface_ready = True - self.processOutgoing(data) - elif len(self.packet_queue) == 0: - self.interface_ready = True + def process_queue(self): + if len(self.packet_queue) > 0: + data = self.packet_queue.pop(0) + self.interface_ready = True + self.processOutgoing(data) + elif len(self.packet_queue) == 0: + self.interface_ready = True - def readLoop(self): - try: - in_frame = False - escape = False - command = KISS.CMD_UNKNOWN - data_buffer = b"" - last_read_ms = int(time.time()*1000) + def readLoop(self): + try: + in_frame = False + escape = False + command = KISS.CMD_UNKNOWN + data_buffer = b"" + last_read_ms = int(time.time()*1000) - while self.serial.is_open: - if self.serial.in_waiting: - byte = ord(self.serial.read(1)) - last_read_ms = int(time.time()*1000) + while self.serial.is_open: + if self.serial.in_waiting: + byte = ord(self.serial.read(1)) + last_read_ms = int(time.time()*1000) - if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA): - in_frame = False - self.processIncoming(data_buffer) - elif (byte == KISS.FEND): - in_frame = True - command = KISS.CMD_UNKNOWN - data_buffer = b"" - elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU): - if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN): - # We only support one HDLC port for now, so - # strip off the port nibble - byte = byte & 0x0F - command = byte - elif (command == KISS.CMD_DATA): - if (byte == KISS.FESC): - escape = True - else: - if (escape): - if (byte == KISS.TFEND): - byte = KISS.FEND - if (byte == KISS.TFESC): - byte = KISS.FESC - escape = False - data_buffer = data_buffer+bytes([byte]) - elif (command == KISS.CMD_READY): - # TODO: add timeout and reset if ready - # command never arrives - self.process_queue() - else: - time_since_last = int(time.time()*1000) - last_read_ms - if len(data_buffer) > 0 and time_since_last > self.timeout: - data_buffer = b"" - in_frame = False - command = KISS.CMD_UNKNOWN - escape = False - sleep(0.08) + if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA): + in_frame = False + self.processIncoming(data_buffer) + elif (byte == KISS.FEND): + in_frame = True + command = KISS.CMD_UNKNOWN + data_buffer = b"" + elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU): + if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN): + # We only support one HDLC port for now, so + # strip off the port nibble + byte = byte & 0x0F + command = byte + elif (command == KISS.CMD_DATA): + if (byte == KISS.FESC): + escape = True + else: + if (escape): + if (byte == KISS.TFEND): + byte = KISS.FEND + if (byte == KISS.TFESC): + byte = KISS.FESC + escape = False + data_buffer = data_buffer+bytes([byte]) + elif (command == KISS.CMD_READY): + # TODO: add timeout and reset if ready + # command never arrives + self.process_queue() + else: + time_since_last = int(time.time()*1000) - last_read_ms + if len(data_buffer) > 0 and time_since_last > self.timeout: + data_buffer = b"" + in_frame = False + command = KISS.CMD_UNKNOWN + escape = False + sleep(0.08) - except Exception as e: - self.online = False - RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) - RNS.log("The interface "+str(self.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR) + except Exception as e: + self.online = False + RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) + RNS.log("The interface "+str(self.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR) - def __str__(self): - return "KISSInterface["+self.name+"]" \ No newline at end of file + def __str__(self): + return "KISSInterface["+self.name+"]" \ No newline at end of file diff --git a/RNS/Interfaces/RNodeInterface.py b/RNS/Interfaces/RNodeInterface.py index fb01b88..b434f03 100644 --- a/RNS/Interfaces/RNodeInterface.py +++ b/RNS/Interfaces/RNodeInterface.py @@ -9,454 +9,454 @@ import math import RNS class KISS(): - FEND = 0xC0 - FESC = 0xDB - TFEND = 0xDC - TFESC = 0xDD - - CMD_UNKNOWN = 0xFE - CMD_DATA = 0x00 - CMD_FREQUENCY = 0x01 - CMD_BANDWIDTH = 0x02 - CMD_TXPOWER = 0x03 - CMD_SF = 0x04 - CMD_CR = 0x05 - CMD_RADIO_STATE = 0x06 - CMD_RADIO_LOCK = 0x07 - CMD_DETECT = 0x08 - CMD_READY = 0x0F - CMD_STAT_RX = 0x21 - CMD_STAT_TX = 0x22 - CMD_STAT_RSSI = 0x23 - CMD_STAT_SNR = 0x24 - CMD_BLINK = 0x30 - CMD_RANDOM = 0x40 - CMD_FW_VERSION = 0x50 - CMD_ROM_READ = 0x51 + FEND = 0xC0 + FESC = 0xDB + TFEND = 0xDC + TFESC = 0xDD + + CMD_UNKNOWN = 0xFE + CMD_DATA = 0x00 + CMD_FREQUENCY = 0x01 + CMD_BANDWIDTH = 0x02 + CMD_TXPOWER = 0x03 + CMD_SF = 0x04 + CMD_CR = 0x05 + CMD_RADIO_STATE = 0x06 + CMD_RADIO_LOCK = 0x07 + CMD_DETECT = 0x08 + CMD_READY = 0x0F + CMD_STAT_RX = 0x21 + CMD_STAT_TX = 0x22 + CMD_STAT_RSSI = 0x23 + CMD_STAT_SNR = 0x24 + CMD_BLINK = 0x30 + CMD_RANDOM = 0x40 + CMD_FW_VERSION = 0x50 + CMD_ROM_READ = 0x51 - DETECT_REQ = 0x73 - DETECT_RESP = 0x46 - - RADIO_STATE_OFF = 0x00 - RADIO_STATE_ON = 0x01 - RADIO_STATE_ASK = 0xFF - - CMD_ERROR = 0x90 - ERROR_INITRADIO = 0x01 - ERROR_TXFAILED = 0x02 - ERROR_EEPROM_LOCKED = 0x03 + DETECT_REQ = 0x73 + DETECT_RESP = 0x46 + + RADIO_STATE_OFF = 0x00 + RADIO_STATE_ON = 0x01 + RADIO_STATE_ASK = 0xFF + + CMD_ERROR = 0x90 + ERROR_INITRADIO = 0x01 + ERROR_TXFAILED = 0x02 + ERROR_EEPROM_LOCKED = 0x03 - @staticmethod - def escape(data): - data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd])) - data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc])) - return data - + @staticmethod + def escape(data): + data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd])) + data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc])) + return data + class RNodeInterface(Interface): - MAX_CHUNK = 32768 + MAX_CHUNK = 32768 - owner = None - port = None - speed = None - databits = None - parity = None - stopbits = None - serial = None + owner = None + port = None + speed = None + databits = None + parity = None + stopbits = None + serial = None - FREQ_MIN = 137000000 - FREQ_MAX = 1020000000 + FREQ_MIN = 137000000 + FREQ_MAX = 1020000000 - RSSI_OFFSET = 157 + RSSI_OFFSET = 157 - CALLSIGN_MAX_LEN = 32 + CALLSIGN_MAX_LEN = 32 - def __init__(self, owner, name, port, frequency = None, bandwidth = None, txpower = None, sf = None, cr = None, flow_control = False, id_interval = None, id_callsign = None): - self.serial = None - self.owner = owner - self.name = name - self.port = port - self.speed = 115200 - self.databits = 8 - self.parity = serial.PARITY_NONE - self.stopbits = 1 - self.timeout = 100 - self.online = False + def __init__(self, owner, name, port, frequency = None, bandwidth = None, txpower = None, sf = None, cr = None, flow_control = False, id_interval = None, id_callsign = None): + self.serial = None + self.owner = owner + self.name = name + self.port = port + self.speed = 115200 + self.databits = 8 + self.parity = serial.PARITY_NONE + self.stopbits = 1 + self.timeout = 100 + self.online = False - self.frequency = frequency - self.bandwidth = bandwidth - self.txpower = txpower - self.sf = sf - self.cr = cr - self.state = KISS.RADIO_STATE_OFF - self.bitrate = 0 + self.frequency = frequency + self.bandwidth = bandwidth + self.txpower = txpower + self.sf = sf + self.cr = cr + self.state = KISS.RADIO_STATE_OFF + self.bitrate = 0 - self.last_id = 0 + self.last_id = 0 - self.r_frequency = None - self.r_bandwidth = None - self.r_txpower = None - self.r_sf = None - self.r_cr = None - self.r_state = None - self.r_lock = None - self.r_stat_rx = None - self.r_stat_tx = None - self.r_stat_rssi = None - self.r_random = None + self.r_frequency = None + self.r_bandwidth = None + self.r_txpower = None + self.r_sf = None + self.r_cr = None + self.r_state = None + self.r_lock = None + self.r_stat_rx = None + self.r_stat_tx = None + self.r_stat_rssi = None + self.r_random = None - self.packet_queue = [] - self.flow_control = flow_control - self.interface_ready = False + self.packet_queue = [] + self.flow_control = flow_control + self.interface_ready = False - self.validcfg = True - if (self.frequency < RNodeInterface.FREQ_MIN or self.frequency > RNodeInterface.FREQ_MAX): - RNS.log("Invalid frequency configured for "+str(self), RNS.LOG_ERROR) - self.validcfg = False + self.validcfg = True + if (self.frequency < RNodeInterface.FREQ_MIN or self.frequency > RNodeInterface.FREQ_MAX): + RNS.log("Invalid frequency configured for "+str(self), RNS.LOG_ERROR) + self.validcfg = False - if (self.txpower < 0 or self.txpower > 17): - RNS.log("Invalid TX power configured for "+str(self), RNS.LOG_ERROR) - self.validcfg = False + if (self.txpower < 0 or self.txpower > 17): + RNS.log("Invalid TX power configured for "+str(self), RNS.LOG_ERROR) + self.validcfg = False - if (self.bandwidth < 7800 or self.bandwidth > 500000): - RNS.log("Invalid bandwidth configured for "+str(self), RNS.LOG_ERROR) - self.validcfg = False + if (self.bandwidth < 7800 or self.bandwidth > 500000): + RNS.log("Invalid bandwidth configured for "+str(self), RNS.LOG_ERROR) + self.validcfg = False - if (self.sf < 7 or self.sf > 12): - RNS.log("Invalid spreading factor configured for "+str(self), RNS.LOG_ERROR) - self.validcfg = False + if (self.sf < 7 or self.sf > 12): + RNS.log("Invalid spreading factor configured for "+str(self), RNS.LOG_ERROR) + self.validcfg = False - if (self.cr < 5 or self.cr > 8): - RNS.log("Invalid coding rate configured for "+str(self), RNS.LOG_ERROR) - self.validcfg = False + if (self.cr < 5 or self.cr > 8): + RNS.log("Invalid coding rate configured for "+str(self), RNS.LOG_ERROR) + self.validcfg = False - if id_interval != None and id_callsign != None: - if (len(id_callsign.encode("utf-8")) <= RNodeInterface.CALLSIGN_MAX_LEN): - self.should_id = True - self.id_callsign = id_callsign - self.id_interval = id_interval - else: - RNS.log("The encoded ID callsign for "+str(self)+" exceeds the max length of "+str(RNodeInterface.CALLSIGN_MAX_LEN)+" bytes.", RNS.LOG_ERROR) - self.validcfg = False - else: - self.id_interval = None - self.id_callsign = None + if id_interval != None and id_callsign != None: + if (len(id_callsign.encode("utf-8")) <= RNodeInterface.CALLSIGN_MAX_LEN): + self.should_id = True + self.id_callsign = id_callsign + self.id_interval = id_interval + else: + RNS.log("The encoded ID callsign for "+str(self)+" exceeds the max length of "+str(RNodeInterface.CALLSIGN_MAX_LEN)+" bytes.", RNS.LOG_ERROR) + self.validcfg = False + else: + self.id_interval = None + self.id_callsign = None - if (not self.validcfg): - raise ValueError("The configuration for "+str(self)+" contains errors, interface is offline") + if (not self.validcfg): + raise ValueError("The configuration for "+str(self)+" contains errors, interface is offline") - try: - RNS.log("Opening serial port "+self.port+"...") - self.serial = serial.Serial( - port = self.port, - baudrate = self.speed, - bytesize = self.databits, - parity = self.parity, - stopbits = self.stopbits, - xonxoff = False, - rtscts = False, - timeout = 0, - inter_byte_timeout = None, - write_timeout = None, - dsrdtr = False, - ) - except Exception as e: - RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR) - raise e + try: + RNS.log("Opening serial port "+self.port+"...") + self.serial = serial.Serial( + port = self.port, + baudrate = self.speed, + bytesize = self.databits, + parity = self.parity, + stopbits = self.stopbits, + xonxoff = False, + rtscts = False, + timeout = 0, + inter_byte_timeout = None, + write_timeout = None, + dsrdtr = False, + ) + except Exception as e: + RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR) + raise e - if self.serial.is_open: - sleep(2.0) - thread = threading.Thread(target=self.readLoop) - thread.setDaemon(True) - thread.start() - self.online = True - RNS.log("Serial port "+self.port+" is now open") - RNS.log("Configuring RNode interface...", RNS.LOG_VERBOSE) - self.initRadio() - if (self.validateRadioState()): - self.interface_ready = True - RNS.log(str(self)+" is configured and powered up") - sleep(1.0) - else: - RNS.log("After configuring "+str(self)+", the reported radio parameters did not match your configuration.", RNS.LOG_ERROR) - RNS.log("Make sure that your hardware actually supports the parameters specified in the configuration", RNS.LOG_ERROR) - RNS.log("Aborting RNode startup", RNS.LOG_ERROR) - self.serial.close() - raise IOError("RNode interface did not pass validation") - else: - raise IOError("Could not open serial port") + if self.serial.is_open: + sleep(2.0) + thread = threading.Thread(target=self.readLoop) + thread.setDaemon(True) + thread.start() + self.online = True + RNS.log("Serial port "+self.port+" is now open") + RNS.log("Configuring RNode interface...", RNS.LOG_VERBOSE) + self.initRadio() + if (self.validateRadioState()): + self.interface_ready = True + RNS.log(str(self)+" is configured and powered up") + sleep(1.0) + else: + RNS.log("After configuring "+str(self)+", the reported radio parameters did not match your configuration.", RNS.LOG_ERROR) + RNS.log("Make sure that your hardware actually supports the parameters specified in the configuration", RNS.LOG_ERROR) + RNS.log("Aborting RNode startup", RNS.LOG_ERROR) + self.serial.close() + raise IOError("RNode interface did not pass validation") + else: + raise IOError("Could not open serial port") - def initRadio(self): - self.setFrequency() - self.setBandwidth() - self.setTXPower() - self.setSpreadingFactor() - self.setCodingRate() - self.setRadioState(KISS.RADIO_STATE_ON) + def initRadio(self): + self.setFrequency() + self.setBandwidth() + self.setTXPower() + self.setSpreadingFactor() + self.setCodingRate() + self.setRadioState(KISS.RADIO_STATE_ON) - def setFrequency(self): - c1 = self.frequency >> 24 - c2 = self.frequency >> 16 & 0xFF - c3 = self.frequency >> 8 & 0xFF - c4 = self.frequency & 0xFF - data = KISS.escape(bytes([c1])+bytes([c2])+bytes([c3])+bytes([c4])) + def setFrequency(self): + c1 = self.frequency >> 24 + c2 = self.frequency >> 16 & 0xFF + c3 = self.frequency >> 8 & 0xFF + c4 = self.frequency & 0xFF + data = KISS.escape(bytes([c1])+bytes([c2])+bytes([c3])+bytes([c4])) - kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FREQUENCY])+data+bytes([KISS.FEND]) - written = self.serial.write(kiss_command) - if written != len(kiss_command): - raise IOError("An IO error occurred while configuring frequency for "+self(str)) + kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FREQUENCY])+data+bytes([KISS.FEND]) + written = self.serial.write(kiss_command) + if written != len(kiss_command): + raise IOError("An IO error occurred while configuring frequency for "+self(str)) - def setBandwidth(self): - c1 = self.bandwidth >> 24 - c2 = self.bandwidth >> 16 & 0xFF - c3 = self.bandwidth >> 8 & 0xFF - c4 = self.bandwidth & 0xFF - data = KISS.escape(bytes([c1])+bytes([c2])+bytes([c3])+bytes([c4])) + def setBandwidth(self): + c1 = self.bandwidth >> 24 + c2 = self.bandwidth >> 16 & 0xFF + c3 = self.bandwidth >> 8 & 0xFF + c4 = self.bandwidth & 0xFF + data = KISS.escape(bytes([c1])+bytes([c2])+bytes([c3])+bytes([c4])) - kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_BANDWIDTH])+data+bytes([KISS.FEND]) - written = self.serial.write(kiss_command) - if written != len(kiss_command): - raise IOError("An IO error occurred while configuring bandwidth for "+self(str)) + kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_BANDWIDTH])+data+bytes([KISS.FEND]) + written = self.serial.write(kiss_command) + if written != len(kiss_command): + raise IOError("An IO error occurred while configuring bandwidth for "+self(str)) - def setTXPower(self): - txp = bytes([self.txpower]) - kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXPOWER])+txp+bytes([KISS.FEND]) - written = self.serial.write(kiss_command) - if written != len(kiss_command): - raise IOError("An IO error occurred while configuring TX power for "+self(str)) + def setTXPower(self): + txp = bytes([self.txpower]) + kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXPOWER])+txp+bytes([KISS.FEND]) + written = self.serial.write(kiss_command) + if written != len(kiss_command): + raise IOError("An IO error occurred while configuring TX power for "+self(str)) - def setSpreadingFactor(self): - sf = bytes([self.sf]) - kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SF])+sf+bytes([KISS.FEND]) - written = self.serial.write(kiss_command) - if written != len(kiss_command): - raise IOError("An IO error occurred while configuring spreading factor for "+self(str)) + def setSpreadingFactor(self): + sf = bytes([self.sf]) + kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SF])+sf+bytes([KISS.FEND]) + written = self.serial.write(kiss_command) + if written != len(kiss_command): + raise IOError("An IO error occurred while configuring spreading factor for "+self(str)) - def setCodingRate(self): - cr = bytes([self.cr]) - kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_CR])+cr+bytes([KISS.FEND]) - written = self.serial.write(kiss_command) - if written != len(kiss_command): - raise IOError("An IO error occurred while configuring coding rate for "+self(str)) + def setCodingRate(self): + cr = bytes([self.cr]) + kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_CR])+cr+bytes([KISS.FEND]) + written = self.serial.write(kiss_command) + if written != len(kiss_command): + raise IOError("An IO error occurred while configuring coding rate for "+self(str)) - def setRadioState(self, state): - kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_RADIO_STATE])+bytes([state])+bytes([KISS.FEND]) - written = self.serial.write(kiss_command) - if written != len(kiss_command): - raise IOError("An IO error occurred while configuring radio state for "+self(str)) + def setRadioState(self, state): + kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_RADIO_STATE])+bytes([state])+bytes([KISS.FEND]) + written = self.serial.write(kiss_command) + if written != len(kiss_command): + raise IOError("An IO error occurred while configuring radio state for "+self(str)) - def validateRadioState(self): - RNS.log("Validating radio configuration for "+str(self)+"...", RNS.LOG_VERBOSE) - sleep(0.25); - if (self.frequency != self.r_frequency): - RNS.log("Frequency mismatch", RNS.LOG_ERROR) - self.validcfg = False - if (self.bandwidth != self.r_bandwidth): - RNS.log("Bandwidth mismatch", RNS.LOG_ERROR) - self.validcfg = False - if (self.txpower != self.r_txpower): - RNS.log("TX power mismatch", RNS.LOG_ERROR) - self.validcfg = False - if (self.sf != self.r_sf): - RNS.log("Spreading factor mismatch", RNS.LOG_ERROR) - self.validcfg = False + def validateRadioState(self): + RNS.log("Validating radio configuration for "+str(self)+"...", RNS.LOG_VERBOSE) + sleep(0.25); + if (self.frequency != self.r_frequency): + RNS.log("Frequency mismatch", RNS.LOG_ERROR) + self.validcfg = False + if (self.bandwidth != self.r_bandwidth): + RNS.log("Bandwidth mismatch", RNS.LOG_ERROR) + self.validcfg = False + if (self.txpower != self.r_txpower): + RNS.log("TX power mismatch", RNS.LOG_ERROR) + self.validcfg = False + if (self.sf != self.r_sf): + RNS.log("Spreading factor mismatch", RNS.LOG_ERROR) + self.validcfg = False - if (self.validcfg): - return True - else: - return False + if (self.validcfg): + return True + else: + return False - def updateBitrate(self): - try: - self.bitrate = self.r_sf * ( (4.0/self.r_cr) / (math.pow(2,self.r_sf)/(self.r_bandwidth/1000)) ) * 1000 - self.bitrate_kbps = round(self.bitrate/1000.0, 2) - RNS.log(str(self)+" On-air bitrate is now "+str(self.bitrate_kbps)+ " kbps", RNS.LOG_INFO) - except: - self.bitrate = 0 + def updateBitrate(self): + try: + self.bitrate = self.r_sf * ( (4.0/self.r_cr) / (math.pow(2,self.r_sf)/(self.r_bandwidth/1000)) ) * 1000 + self.bitrate_kbps = round(self.bitrate/1000.0, 2) + RNS.log(str(self)+" On-air bitrate is now "+str(self.bitrate_kbps)+ " kbps", RNS.LOG_INFO) + except: + self.bitrate = 0 - def processIncoming(self, data): - self.owner.inbound(data, self) + def processIncoming(self, data): + self.owner.inbound(data, self) - def processOutgoing(self,data): - if self.online: - if self.interface_ready: - if self.flow_control: - self.interface_ready = False + def processOutgoing(self,data): + if self.online: + if self.interface_ready: + if self.flow_control: + self.interface_ready = False - frame = b"" + frame = b"" - if self.id_interval != None and self.id_callsign != None: - if self.last_id + self.id_interval < time.time(): - self.last_id = time.time() - frame = bytes([0xc0])+bytes([0x00])+KISS.escape(self.id_callsign.encode("utf-8"))+bytes([0xc0]) + if self.id_interval != None and self.id_callsign != None: + if self.last_id + self.id_interval < time.time(): + self.last_id = time.time() + frame = bytes([0xc0])+bytes([0x00])+KISS.escape(self.id_callsign.encode("utf-8"))+bytes([0xc0]) - data = KISS.escape(data) - frame += bytes([0xc0])+bytes([0x00])+data+bytes([0xc0]) - written = self.serial.write(frame) + data = KISS.escape(data) + frame += bytes([0xc0])+bytes([0x00])+data+bytes([0xc0]) + written = self.serial.write(frame) - if written != len(frame): - raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data))) - else: - self.queue(data) + if written != len(frame): + raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data))) + else: + self.queue(data) - def queue(self, data): - self.packet_queue.append(data) + def queue(self, data): + self.packet_queue.append(data) - def process_queue(self): - if len(self.packet_queue) > 0: - data = self.packet_queue.pop(0) - self.interface_ready = True - self.processOutgoing(data) - elif len(self.packet_queue) == 0: - self.interface_ready = True + def process_queue(self): + if len(self.packet_queue) > 0: + data = self.packet_queue.pop(0) + self.interface_ready = True + self.processOutgoing(data) + elif len(self.packet_queue) == 0: + self.interface_ready = True - def readLoop(self): - try: - in_frame = False - escape = False - command = KISS.CMD_UNKNOWN - data_buffer = b"" - command_buffer = b"" - last_read_ms = int(time.time()*1000) + def readLoop(self): + try: + in_frame = False + escape = False + command = KISS.CMD_UNKNOWN + data_buffer = b"" + command_buffer = b"" + last_read_ms = int(time.time()*1000) - while self.serial.is_open: - if self.serial.in_waiting: - byte = ord(self.serial.read(1)) - last_read_ms = int(time.time()*1000) + while self.serial.is_open: + if self.serial.in_waiting: + byte = ord(self.serial.read(1)) + last_read_ms = int(time.time()*1000) - if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA): - in_frame = False - self.processIncoming(data_buffer) - data_buffer = b"" - command_buffer = b"" - elif (byte == KISS.FEND): - in_frame = True - command = KISS.CMD_UNKNOWN - data_buffer = b"" - command_buffer = b"" - elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU): - if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN): - command = byte - elif (command == KISS.CMD_DATA): - if (byte == KISS.FESC): - escape = True - else: - if (escape): - if (byte == KISS.TFEND): - byte = KISS.FEND - if (byte == KISS.TFESC): - byte = KISS.FESC - escape = False - data_buffer = data_buffer+bytes([byte]) - elif (command == KISS.CMD_FREQUENCY): - if (byte == KISS.FESC): - escape = True - else: - if (escape): - if (byte == KISS.TFEND): - byte = KISS.FEND - if (byte == KISS.TFESC): - byte = KISS.FESC - escape = False - command_buffer = command_buffer+bytes([byte]) - if (len(command_buffer) == 4): - self.r_frequency = command_buffer[0] << 24 | command_buffer[1] << 16 | command_buffer[2] << 8 | command_buffer[3] - RNS.log(str(self)+" Radio reporting frequency is "+str(self.r_frequency/1000000.0)+" MHz", RNS.LOG_DEBUG) - self.updateBitrate() + if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA): + in_frame = False + self.processIncoming(data_buffer) + data_buffer = b"" + command_buffer = b"" + elif (byte == KISS.FEND): + in_frame = True + command = KISS.CMD_UNKNOWN + data_buffer = b"" + command_buffer = b"" + elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU): + if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN): + command = byte + elif (command == KISS.CMD_DATA): + if (byte == KISS.FESC): + escape = True + else: + if (escape): + if (byte == KISS.TFEND): + byte = KISS.FEND + if (byte == KISS.TFESC): + byte = KISS.FESC + escape = False + data_buffer = data_buffer+bytes([byte]) + elif (command == KISS.CMD_FREQUENCY): + if (byte == KISS.FESC): + escape = True + else: + if (escape): + if (byte == KISS.TFEND): + byte = KISS.FEND + if (byte == KISS.TFESC): + byte = KISS.FESC + escape = False + command_buffer = command_buffer+bytes([byte]) + if (len(command_buffer) == 4): + self.r_frequency = command_buffer[0] << 24 | command_buffer[1] << 16 | command_buffer[2] << 8 | command_buffer[3] + RNS.log(str(self)+" Radio reporting frequency is "+str(self.r_frequency/1000000.0)+" MHz", RNS.LOG_DEBUG) + self.updateBitrate() - elif (command == KISS.CMD_BANDWIDTH): - if (byte == KISS.FESC): - escape = True - else: - if (escape): - if (byte == KISS.TFEND): - byte = KISS.FEND - if (byte == KISS.TFESC): - byte = KISS.FESC - escape = False - command_buffer = command_buffer+bytes([byte]) - if (len(command_buffer) == 4): - self.r_bandwidth = command_buffer[0] << 24 | command_buffer[1] << 16 | command_buffer[2] << 8 | command_buffer[3] - RNS.log(str(self)+" Radio reporting bandwidth is "+str(self.r_bandwidth/1000.0)+" KHz", RNS.LOG_DEBUG) - self.updateBitrate() + elif (command == KISS.CMD_BANDWIDTH): + if (byte == KISS.FESC): + escape = True + else: + if (escape): + if (byte == KISS.TFEND): + byte = KISS.FEND + if (byte == KISS.TFESC): + byte = KISS.FESC + escape = False + command_buffer = command_buffer+bytes([byte]) + if (len(command_buffer) == 4): + self.r_bandwidth = command_buffer[0] << 24 | command_buffer[1] << 16 | command_buffer[2] << 8 | command_buffer[3] + RNS.log(str(self)+" Radio reporting bandwidth is "+str(self.r_bandwidth/1000.0)+" KHz", RNS.LOG_DEBUG) + self.updateBitrate() - elif (command == KISS.CMD_TXPOWER): - self.r_txpower = byte - RNS.log(str(self)+" Radio reporting TX power is "+str(self.r_txpower)+" dBm", RNS.LOG_DEBUG) - elif (command == KISS.CMD_SF): - self.r_sf = byte - RNS.log(str(self)+" Radio reporting spreading factor is "+str(self.r_sf), RNS.LOG_DEBUG) - self.updateBitrate() - elif (command == KISS.CMD_CR): - self.r_cr = byte - RNS.log(str(self)+" Radio reporting coding rate is "+str(self.r_cr), RNS.LOG_DEBUG) - self.updateBitrate() - elif (command == KISS.CMD_RADIO_STATE): - self.r_state = byte - elif (command == KISS.CMD_RADIO_LOCK): - self.r_lock = byte - elif (command == KISS.CMD_STAT_RX): - if (byte == KISS.FESC): - escape = True - else: - if (escape): - if (byte == KISS.TFEND): - byte = KISS.FEND - if (byte == KISS.TFESC): - byte = KISS.FESC - escape = False - command_buffer = command_buffer+bytes([byte]) - if (len(command_buffer) == 4): - self.r_stat_rx = ord(command_buffer[0]) << 24 | ord(command_buffer[1]) << 16 | ord(command_buffer[2]) << 8 | ord(command_buffer[3]) + elif (command == KISS.CMD_TXPOWER): + self.r_txpower = byte + RNS.log(str(self)+" Radio reporting TX power is "+str(self.r_txpower)+" dBm", RNS.LOG_DEBUG) + elif (command == KISS.CMD_SF): + self.r_sf = byte + RNS.log(str(self)+" Radio reporting spreading factor is "+str(self.r_sf), RNS.LOG_DEBUG) + self.updateBitrate() + elif (command == KISS.CMD_CR): + self.r_cr = byte + RNS.log(str(self)+" Radio reporting coding rate is "+str(self.r_cr), RNS.LOG_DEBUG) + self.updateBitrate() + elif (command == KISS.CMD_RADIO_STATE): + self.r_state = byte + elif (command == KISS.CMD_RADIO_LOCK): + self.r_lock = byte + elif (command == KISS.CMD_STAT_RX): + if (byte == KISS.FESC): + escape = True + else: + if (escape): + if (byte == KISS.TFEND): + byte = KISS.FEND + if (byte == KISS.TFESC): + byte = KISS.FESC + escape = False + command_buffer = command_buffer+bytes([byte]) + if (len(command_buffer) == 4): + self.r_stat_rx = ord(command_buffer[0]) << 24 | ord(command_buffer[1]) << 16 | ord(command_buffer[2]) << 8 | ord(command_buffer[3]) - elif (command == KISS.CMD_STAT_TX): - if (byte == KISS.FESC): - escape = True - else: - if (escape): - if (byte == KISS.TFEND): - byte = KISS.FEND - if (byte == KISS.TFESC): - byte = KISS.FESC - escape = False - command_buffer = command_buffer+bytes([byte]) - if (len(command_buffer) == 4): - self.r_stat_tx = ord(command_buffer[0]) << 24 | ord(command_buffer[1]) << 16 | ord(command_buffer[2]) << 8 | ord(command_buffer[3]) + elif (command == KISS.CMD_STAT_TX): + if (byte == KISS.FESC): + escape = True + else: + if (escape): + if (byte == KISS.TFEND): + byte = KISS.FEND + if (byte == KISS.TFESC): + byte = KISS.FESC + escape = False + command_buffer = command_buffer+bytes([byte]) + if (len(command_buffer) == 4): + self.r_stat_tx = ord(command_buffer[0]) << 24 | ord(command_buffer[1]) << 16 | ord(command_buffer[2]) << 8 | ord(command_buffer[3]) - elif (command == KISS.CMD_STAT_RSSI): - self.r_stat_rssi = byte-RNodeInterface.RSSI_OFFSET - elif (command == KISS.CMD_STAT_SNR): - self.r_stat_snr = int.from_bytes(bytes([byte]), byteorder="big", signed=True) * 0.25 - elif (command == KISS.CMD_RANDOM): - self.r_random = byte - elif (command == KISS.CMD_ERROR): - if (byte == KISS.ERROR_INITRADIO): - RNS.log(str(self)+" hardware initialisation error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR) - elif (byte == KISS.ERROR_INITRADIO): - RNS.log(str(self)+" hardware TX error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR) - else: - RNS.log(str(self)+" hardware error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR) - elif (command == KISS.CMD_READY): - self.process_queue() - - else: - time_since_last = int(time.time()*1000) - last_read_ms - if len(data_buffer) > 0 and time_since_last > self.timeout: - RNS.log(str(self)+" serial read timeout", RNS.LOG_DEBUG) - data_buffer = b"" - in_frame = False - command = KISS.CMD_UNKNOWN - escape = False - sleep(0.08) + elif (command == KISS.CMD_STAT_RSSI): + self.r_stat_rssi = byte-RNodeInterface.RSSI_OFFSET + elif (command == KISS.CMD_STAT_SNR): + self.r_stat_snr = int.from_bytes(bytes([byte]), byteorder="big", signed=True) * 0.25 + elif (command == KISS.CMD_RANDOM): + self.r_random = byte + elif (command == KISS.CMD_ERROR): + if (byte == KISS.ERROR_INITRADIO): + RNS.log(str(self)+" hardware initialisation error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR) + elif (byte == KISS.ERROR_INITRADIO): + RNS.log(str(self)+" hardware TX error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR) + else: + RNS.log(str(self)+" hardware error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR) + elif (command == KISS.CMD_READY): + self.process_queue() + + else: + time_since_last = int(time.time()*1000) - last_read_ms + if len(data_buffer) > 0 and time_since_last > self.timeout: + RNS.log(str(self)+" serial read timeout", RNS.LOG_DEBUG) + data_buffer = b"" + in_frame = False + command = KISS.CMD_UNKNOWN + escape = False + sleep(0.08) - except Exception as e: - self.online = False - RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) - RNS.log("The interface "+str(self.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR) + except Exception as e: + self.online = False + RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) + RNS.log("The interface "+str(self.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR) - def __str__(self): - return "RNodeInterface["+self.name+"]" + def __str__(self): + return "RNodeInterface["+self.name+"]" diff --git a/RNS/Interfaces/SerialInterface.py b/RNS/Interfaces/SerialInterface.py index f674266..d33ec12 100755 --- a/RNS/Interfaces/SerialInterface.py +++ b/RNS/Interfaces/SerialInterface.py @@ -7,130 +7,130 @@ import time import RNS class HDLC(): - # The Serial Interface packetizes data using - # simplified HDLC framing, similar to PPP - FLAG = 0x7E - ESC = 0x7D - ESC_MASK = 0x20 + # The Serial Interface packetizes data using + # simplified HDLC framing, similar to PPP + FLAG = 0x7E + ESC = 0x7D + ESC_MASK = 0x20 - @staticmethod - def escape(data): - data = data.replace(bytes([HDLC.ESC]), bytes([HDLC.ESC, HDLC.ESC^HDLC.ESC_MASK])) - data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK])) - return data + @staticmethod + def escape(data): + data = data.replace(bytes([HDLC.ESC]), bytes([HDLC.ESC, HDLC.ESC^HDLC.ESC_MASK])) + data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK])) + return data class SerialInterface(Interface): - MAX_CHUNK = 32768 + MAX_CHUNK = 32768 - owner = None - port = None - speed = None - databits = None - parity = None - stopbits = None - serial = None + owner = None + port = None + speed = None + databits = None + parity = None + stopbits = None + serial = None - def __init__(self, owner, name, port, speed, databits, parity, stopbits): - self.serial = None - self.owner = owner - self.name = name - self.port = port - self.speed = speed - self.databits = databits - self.parity = serial.PARITY_NONE - self.stopbits = stopbits - self.timeout = 100 - self.online = False + def __init__(self, owner, name, port, speed, databits, parity, stopbits): + self.serial = None + self.owner = owner + self.name = name + self.port = port + self.speed = speed + self.databits = databits + self.parity = serial.PARITY_NONE + self.stopbits = stopbits + self.timeout = 100 + self.online = False - if parity.lower() == "e" or parity.lower() == "even": - self.parity = serial.PARITY_EVEN + if parity.lower() == "e" or parity.lower() == "even": + self.parity = serial.PARITY_EVEN - if parity.lower() == "o" or parity.lower() == "odd": - self.parity = serial.PARITY_ODD + if parity.lower() == "o" or parity.lower() == "odd": + self.parity = serial.PARITY_ODD - try: - RNS.log("Opening serial port "+self.port+"...") - self.serial = serial.Serial( - port = self.port, - baudrate = self.speed, - bytesize = self.databits, - parity = self.parity, - stopbits = self.stopbits, - xonxoff = False, - rtscts = False, - timeout = 0, - inter_byte_timeout = None, - write_timeout = None, - dsrdtr = False, - ) - except Exception as e: - RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR) - raise e + try: + RNS.log("Opening serial port "+self.port+"...") + self.serial = serial.Serial( + port = self.port, + baudrate = self.speed, + bytesize = self.databits, + parity = self.parity, + stopbits = self.stopbits, + xonxoff = False, + rtscts = False, + timeout = 0, + inter_byte_timeout = None, + write_timeout = None, + dsrdtr = False, + ) + except Exception as e: + RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR) + raise e - if self.serial.is_open: - sleep(0.5) - thread = threading.Thread(target=self.readLoop) - thread.setDaemon(True) - thread.start() - self.online = True - RNS.log("Serial port "+self.port+" is now open") - else: - raise IOError("Could not open serial port") + if self.serial.is_open: + sleep(0.5) + thread = threading.Thread(target=self.readLoop) + thread.setDaemon(True) + thread.start() + self.online = True + RNS.log("Serial port "+self.port+" is now open") + else: + raise IOError("Could not open serial port") - def processIncoming(self, data): - self.owner.inbound(data, self) + def processIncoming(self, data): + self.owner.inbound(data, self) - def processOutgoing(self,data): - if self.online: - data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG]) - written = self.serial.write(data) - if written != len(data): - raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data))) + def processOutgoing(self,data): + if self.online: + data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG]) + written = self.serial.write(data) + if written != len(data): + raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data))) - def readLoop(self): - try: - in_frame = False - escape = False - data_buffer = b"" - last_read_ms = int(time.time()*1000) + def readLoop(self): + try: + in_frame = False + escape = False + data_buffer = b"" + last_read_ms = int(time.time()*1000) - while self.serial.is_open: - if self.serial.in_waiting: - byte = ord(self.serial.read(1)) - last_read_ms = int(time.time()*1000) + while self.serial.is_open: + if self.serial.in_waiting: + byte = ord(self.serial.read(1)) + last_read_ms = int(time.time()*1000) - if (in_frame and byte == HDLC.FLAG): - in_frame = False - self.processIncoming(data_buffer) - elif (byte == HDLC.FLAG): - in_frame = True - data_buffer = b"" - elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU): - if (byte == HDLC.ESC): - escape = True - else: - if (escape): - if (byte == HDLC.FLAG ^ HDLC.ESC_MASK): - byte = HDLC.FLAG - if (byte == HDLC.ESC ^ HDLC.ESC_MASK): - byte = HDLC.ESC - escape = False - data_buffer = data_buffer+bytes([byte]) - - else: - time_since_last = int(time.time()*1000) - last_read_ms - if len(data_buffer) > 0 and time_since_last > self.timeout: - data_buffer = b"" - in_frame = False - escape = False - sleep(0.08) - except Exception as e: - self.online = False - RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) - RNS.log("The interface "+str(self.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR) + if (in_frame and byte == HDLC.FLAG): + in_frame = False + self.processIncoming(data_buffer) + elif (byte == HDLC.FLAG): + in_frame = True + data_buffer = b"" + elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU): + if (byte == HDLC.ESC): + escape = True + else: + if (escape): + if (byte == HDLC.FLAG ^ HDLC.ESC_MASK): + byte = HDLC.FLAG + if (byte == HDLC.ESC ^ HDLC.ESC_MASK): + byte = HDLC.ESC + escape = False + data_buffer = data_buffer+bytes([byte]) + + else: + time_since_last = int(time.time()*1000) - last_read_ms + if len(data_buffer) > 0 and time_since_last > self.timeout: + data_buffer = b"" + in_frame = False + escape = False + sleep(0.08) + except Exception as e: + self.online = False + RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) + RNS.log("The interface "+str(self.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR) - def __str__(self): - return "SerialInterface["+self.name+"]" + def __str__(self): + return "SerialInterface["+self.name+"]" diff --git a/RNS/Link.py b/RNS/Link.py index 840cc0d..c47ebfb 100644 --- a/RNS/Link.py +++ b/RNS/Link.py @@ -15,521 +15,521 @@ import RNS import traceback class LinkCallbacks: - def __init__(self): - self.link_established = None - self.link_closed = None - self.packet = None - self.resource_started = None - self.resource_concluded = None + def __init__(self): + self.link_established = None + self.link_closed = None + self.packet = None + self.resource_started = None + self.resource_concluded = None class Link: - CURVE = ec.SECP256R1() - ECPUBSIZE = 91 - BLOCKSIZE = 16 - AES_HMAC_OVERHEAD = 58 - MDU = math.floor((RNS.Reticulum.MDU-AES_HMAC_OVERHEAD)/BLOCKSIZE)*BLOCKSIZE - 1 - - # TODO: This should not be hardcoded, - # but calculated from something like - # first-hop RTT latency and distance - DEFAULT_TIMEOUT = 15.0 - TIMEOUT_FACTOR = 3 - STALE_GRACE = 2 - KEEPALIVE = 180 - - 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 - def validateRequest(owner, data, packet): - if len(data) == (Link.ECPUBSIZE): - try: - link = Link(owner = owner, peer_pub_bytes = data[:Link.ECPUBSIZE]) - link.setLinkID(packet) - link.destination = packet.destination - RNS.log("Validating link request "+RNS.prettyhexrep(link.link_id), RNS.LOG_VERBOSE) - link.handshake() - link.attached_interface = packet.receiving_interface - link.prove() - link.request_time = time.time() - RNS.Transport.registerLink(link) - link.last_inbound = time.time() - link.start_watchdog() - - # TODO: Why was link_established callback here? Seems weird - # to call this before RTT packet has been received - #if self.owner.callbacks.link_established != None: - # self.owner.callbacks.link_established(link) - - RNS.log("Incoming link request "+str(link)+" accepted", RNS.LOG_VERBOSE) - return link - - except Exception as e: - RNS.log("Validating link request failed", RNS.LOG_VERBOSE) - traceback.print_exc() - return None - - else: - RNS.log("Invalid link request payload size, dropping request", RNS.LOG_VERBOSE) - return None - - - def __init__(self, destination=None, owner=None, peer_pub_bytes = None): - if destination != None and destination.type != RNS.Destination.SINGLE: - raise TypeError("Links can only be established to the \"single\" destination type") - self.rtt = None - self.callbacks = LinkCallbacks() - self.resource_strategy = Link.ACCEPT_NONE - self.outgoing_resources = [] - self.incoming_resources = [] - self.last_inbound = 0 - self.last_outbound = 0 - self.tx = 0 - self.rx = 0 - self.txbytes = 0 - self.rxbytes = 0 - self.default_timeout = Link.DEFAULT_TIMEOUT - self.proof_timeout = self.default_timeout - self.timeout_factor = Link.TIMEOUT_FACTOR - self.keepalive = Link.KEEPALIVE - self.watchdog_lock = False - self.status = Link.PENDING - self.type = RNS.Destination.LINK - self.owner = owner - self.destination = destination - self.attached_interface = None - self.__encryption_disabled = False - if self.destination == None: - self.initiator = False - else: - self.initiator = True - - self.prv = ec.generate_private_key(Link.CURVE, default_backend()) - self.pub = self.prv.public_key() - self.pub_bytes = self.pub.public_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PublicFormat.SubjectPublicKeyInfo - ) - - if peer_pub_bytes == None: - self.peer_pub = None - self.peer_pub_bytes = None - else: - self.loadPeer(peer_pub_bytes) - - if (self.initiator): - self.request_data = self.pub_bytes - self.packet = RNS.Packet(destination, self.request_data, packet_type=RNS.Packet.LINKREQUEST) - self.packet.pack() - self.setLinkID(self.packet) - RNS.Transport.registerLink(self) - self.request_time = time.time() - self.start_watchdog() - self.packet.send() - RNS.log("Link request "+RNS.prettyhexrep(self.link_id)+" sent to "+str(self.destination), RNS.LOG_VERBOSE) - - - def loadPeer(self, peer_pub_bytes): - self.peer_pub_bytes = peer_pub_bytes - self.peer_pub = serialization.load_der_public_key(peer_pub_bytes, backend=default_backend()) - if not hasattr(self.peer_pub, "curve"): - self.peer_pub.curve = Link.CURVE - - def setLinkID(self, packet): - self.link_id = packet.getTruncatedHash() - self.hash = self.link_id - - def handshake(self): - self.status = Link.HANDSHAKE - self.shared_key = self.prv.exchange(ec.ECDH(), self.peer_pub) - self.derived_key = HKDF( - algorithm=hashes.SHA256(), - length=32, - salt=self.getSalt(), - info=self.getContext(), - backend=default_backend() - ).derive(self.shared_key) - - def prove(self): - signed_data = self.link_id+self.pub_bytes - signature = self.owner.identity.sign(signed_data) - - proof_data = self.pub_bytes+signature - proof = RNS.Packet(self, proof_data, packet_type=RNS.Packet.PROOF, context=RNS.Packet.LRPROOF) - 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): - if self.initiator: - peer_pub_bytes = packet.data[:Link.ECPUBSIZE] - signed_data = self.link_id+peer_pub_bytes - signature = packet.data[Link.ECPUBSIZE:RNS.Identity.KEYSIZE//8+Link.ECPUBSIZE] - - if self.destination.identity.validate(signature, signed_data): - self.loadPeer(peer_pub_bytes) - self.handshake() - self.rtt = time.time() - self.request_time - self.attached_interface = packet.receiving_interface - RNS.Transport.activateLink(self) - RNS.log("Link "+str(self)+" established with "+str(self.destination)+", RTT is "+str(self.rtt), RNS.LOG_VERBOSE) - rtt_data = umsgpack.packb(self.rtt) - rtt_packet = RNS.Packet(self, rtt_data, context=RNS.Packet.LRRTT) - RNS.log("Sending RTT packet", RNS.LOG_EXTREME); - rtt_packet.send() - - self.status = Link.ACTIVE - if self.callbacks.link_established != None: - thread = threading.Thread(target=self.callbacks.link_established, args=(self,)) - thread.setDaemon(True) - thread.start() - else: - RNS.log("Invalid link proof signature received by "+str(self), RNS.LOG_VERBOSE) - # TODO: should we really do this, or just wait - # for a valid one? Needs analysis. - self.teardown() - - - def rtt_packet(self, packet): - try: - # TODO: This is crude, we should use the delta - # to model a more representative per-bit round - # trip time, and use that to set a sensible RTT - # expectancy for the link. This will have to do - # for now though. - measured_rtt = time.time() - self.request_time - plaintext = self.decrypt(packet.data) - rtt = umsgpack.unpackb(plaintext) - self.rtt = max(measured_rtt, rtt) - self.status = Link.ACTIVE - - if self.owner.callbacks.link_established != None: - self.owner.callbacks.link_established(self) - except Exception as e: - RNS.log("Error occurred while processing RTT packet, tearing down link", RNS.LOG_ERROR) - traceback.print_exc() - self.teardown() - - def getSalt(self): - return self.link_id - - def getContext(self): - return None - - def teardown(self): - 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() - 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 - 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() - - self.prv = None - self.pub = None - self.pub_bytes = None - self.shared_key = None - self.derived_key = None - - if self.callbacks.link_closed != None: - self.callbacks.link_closed(self) - - def start_watchdog(self): - thread = threading.Thread(target=self.__watchdog_job) - thread.setDaemon(True) - thread.start() - - def __watchdog_job(self): - while not self.status == Link.CLOSED: - while (self.watchdog_lock): - sleep(max(self.rtt, 0.025)) - - if not self.status == Link.CLOSED: - # Link was initiated, but no response - # from destination yet - if self.status == Link.PENDING: - next_check = self.request_time + self.proof_timeout - sleep_time = next_check - time.time() - if time.time() >= self.request_time + self.proof_timeout: - 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: - next_check = self.request_time + self.proof_timeout - sleep_time = next_check - time.time() - if time.time() >= self.request_time + self.proof_timeout: - RNS.log("Timeout waiting for RTT packet from link initiator", RNS.LOG_DEBUG) - self.status = Link.CLOSED - self.teardown_reason = Link.TIMEOUT - self.link_closed() - sleep_time = 0.001 - - elif self.status == Link.ACTIVE: - if time.time() >= self.last_inbound + self.keepalive: - sleep_time = self.rtt * self.timeout_factor + Link.STALE_GRACE - self.status = Link.STALE - if self.initiator: - self.send_keepalive() - else: - sleep_time = (self.last_inbound + self.keepalive) - time.time() - - 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() - - sleep(sleep_time) - - - def send_keepalive(self): - keepalive_packet = RNS.Packet(self, bytes([0xFF]), context=RNS.Packet.KEEPALIVE) - keepalive_packet.send() - - 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: - RNS.log("Link-associated packet received on unexpected interface! Someone might be trying to manipulate your communication!", RNS.LOG_ERROR) - else: - self.last_inbound = time.time() - self.rx += 1 - self.rxbytes += len(packet.data) - if self.status == Link.STALE: - self.status = Link.ACTIVE - - if packet.packet_type == RNS.Packet.DATA: - if packet.context == RNS.Packet.NONE: - plaintext = self.decrypt(packet.data) - if self.callbacks.packet != None: - thread = threading.Thread(target=self.callbacks.packet, args=(plaintext, packet)) - thread.setDaemon(True) - thread.start() - - 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: - if not self.initiator: - self.rtt_packet(packet) - - elif packet.context == RNS.Packet.LINKCLOSE: - self.teardown_packet(packet) - - elif packet.context == RNS.Packet.RESOURCE_ADV: - packet.plaintext = self.decrypt(packet.data) - if self.resource_strategy == Link.ACCEPT_NONE: - pass - elif self.resource_strategy == Link.ACCEPT_APP: - if self.callbacks.resource != None: - thread = threading.Thread(target=self.callbacks.resource, args=(packet)) - thread.setDaemon(True) - thread.start() - elif self.resource_strategy == Link.ACCEPT_ALL: - RNS.Resource.accept(packet, self.callbacks.resource_concluded) - - elif packet.context == RNS.Packet.RESOURCE_REQ: - plaintext = self.decrypt(packet.data) - 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: - resource.request(plaintext) - - elif packet.context == RNS.Packet.RESOURCE_HMU: - plaintext = self.decrypt(packet.data) - resource_hash = plaintext[:RNS.Identity.HASHLENGTH//8] - for resource in self.incoming_resources: - if resource_hash == resource.hash: - resource.hashmap_update_packet(plaintext) - - elif packet.context == RNS.Packet.RESOURCE_ICL: - plaintext = self.decrypt(packet.data) - resource_hash = plaintext[:RNS.Identity.HASHLENGTH//8] - for resource in self.incoming_resources: - if resource_hash == resource.hash: - resource.cancel() - - 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() - - - # 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) - - 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: - resource.validateProof(packet.data) - - self.watchdog_lock = False - - - def encrypt(self, plaintext): - if self.__encryption_disabled: - return plaintext - try: - # TODO: Optimise this re-allocation - fernet = Fernet(base64.urlsafe_b64encode(self.derived_key)) - ciphertext = base64.urlsafe_b64decode(fernet.encrypt(plaintext)) - return ciphertext - except Exception as e: - RNS.log("Encryption on link "+str(self)+" failed. The contained exception was: "+str(e), RNS.LOG_ERROR) - - - def decrypt(self, ciphertext): - if self.__encryption_disabled: - return ciphertext - try: - fernet = Fernet(base64.urlsafe_b64encode(self.derived_key)) - plaintext = fernet.decrypt(base64.urlsafe_b64encode(ciphertext)) - return plaintext - except Exception as e: - RNS.log("Decryption failed on link "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR) - traceback.print_exc() - - 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): - self.callbacks.link_established = callback - - def link_closed_callback(self, callback): - self.callbacks.link_closed = callback - - def packet_callback(self, callback): - self.callbacks.packet = callback - - # Called when an incoming resource transfer is started - def resource_started_callback(self, callback): - self.callbacks.resource_started = callback - - # Called when a resource transfer is concluded - def resource_concluded_callback(self, callback): - self.callbacks.resource_concluded = callback - - 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: - 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) - - 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 disableEncryption(self): - if (RNS.Reticulum.should_allow_unencrypted()): - RNS.log("The link "+str(self)+" was downgraded to an encryptionless link", RNS.LOG_NOTICE) - self.__encryption_disabled = True - else: - RNS.log("Attempt to disable encryption on link, but encryptionless links are not allowed by config.", RNS.LOG_CRITICAL) - RNS.log("Shutting down Reticulum now!", RNS.LOG_CRITICAL) - RNS.panic() - - def encryption_disabled(self): - return self.__encryption_disabled - - def __str__(self): - return RNS.prettyhexrep(self.link_id) \ No newline at end of file + CURVE = ec.SECP256R1() + ECPUBSIZE = 91 + BLOCKSIZE = 16 + AES_HMAC_OVERHEAD = 58 + MDU = math.floor((RNS.Reticulum.MDU-AES_HMAC_OVERHEAD)/BLOCKSIZE)*BLOCKSIZE - 1 + + # TODO: This should not be hardcoded, + # but calculated from something like + # first-hop RTT latency and distance + DEFAULT_TIMEOUT = 15.0 + TIMEOUT_FACTOR = 3 + STALE_GRACE = 2 + KEEPALIVE = 180 + + 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 + def validateRequest(owner, data, packet): + if len(data) == (Link.ECPUBSIZE): + try: + link = Link(owner = owner, peer_pub_bytes = data[:Link.ECPUBSIZE]) + link.setLinkID(packet) + link.destination = packet.destination + RNS.log("Validating link request "+RNS.prettyhexrep(link.link_id), RNS.LOG_VERBOSE) + link.handshake() + link.attached_interface = packet.receiving_interface + link.prove() + link.request_time = time.time() + RNS.Transport.registerLink(link) + link.last_inbound = time.time() + link.start_watchdog() + + # TODO: Why was link_established callback here? Seems weird + # to call this before RTT packet has been received + #if self.owner.callbacks.link_established != None: + # self.owner.callbacks.link_established(link) + + RNS.log("Incoming link request "+str(link)+" accepted", RNS.LOG_VERBOSE) + return link + + except Exception as e: + RNS.log("Validating link request failed", RNS.LOG_VERBOSE) + traceback.print_exc() + return None + + else: + RNS.log("Invalid link request payload size, dropping request", RNS.LOG_VERBOSE) + return None + + + def __init__(self, destination=None, owner=None, peer_pub_bytes = None): + if destination != None and destination.type != RNS.Destination.SINGLE: + raise TypeError("Links can only be established to the \"single\" destination type") + self.rtt = None + self.callbacks = LinkCallbacks() + self.resource_strategy = Link.ACCEPT_NONE + self.outgoing_resources = [] + self.incoming_resources = [] + self.last_inbound = 0 + self.last_outbound = 0 + self.tx = 0 + self.rx = 0 + self.txbytes = 0 + self.rxbytes = 0 + self.default_timeout = Link.DEFAULT_TIMEOUT + self.proof_timeout = self.default_timeout + self.timeout_factor = Link.TIMEOUT_FACTOR + self.keepalive = Link.KEEPALIVE + self.watchdog_lock = False + self.status = Link.PENDING + self.type = RNS.Destination.LINK + self.owner = owner + self.destination = destination + self.attached_interface = None + self.__encryption_disabled = False + if self.destination == None: + self.initiator = False + else: + self.initiator = True + + self.prv = ec.generate_private_key(Link.CURVE, default_backend()) + self.pub = self.prv.public_key() + self.pub_bytes = self.pub.public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + + if peer_pub_bytes == None: + self.peer_pub = None + self.peer_pub_bytes = None + else: + self.loadPeer(peer_pub_bytes) + + if (self.initiator): + self.request_data = self.pub_bytes + self.packet = RNS.Packet(destination, self.request_data, packet_type=RNS.Packet.LINKREQUEST) + self.packet.pack() + self.setLinkID(self.packet) + RNS.Transport.registerLink(self) + self.request_time = time.time() + self.start_watchdog() + self.packet.send() + RNS.log("Link request "+RNS.prettyhexrep(self.link_id)+" sent to "+str(self.destination), RNS.LOG_VERBOSE) + + + def loadPeer(self, peer_pub_bytes): + self.peer_pub_bytes = peer_pub_bytes + self.peer_pub = serialization.load_der_public_key(peer_pub_bytes, backend=default_backend()) + if not hasattr(self.peer_pub, "curve"): + self.peer_pub.curve = Link.CURVE + + def setLinkID(self, packet): + self.link_id = packet.getTruncatedHash() + self.hash = self.link_id + + def handshake(self): + self.status = Link.HANDSHAKE + self.shared_key = self.prv.exchange(ec.ECDH(), self.peer_pub) + self.derived_key = HKDF( + algorithm=hashes.SHA256(), + length=32, + salt=self.getSalt(), + info=self.getContext(), + backend=default_backend() + ).derive(self.shared_key) + + def prove(self): + signed_data = self.link_id+self.pub_bytes + signature = self.owner.identity.sign(signed_data) + + proof_data = self.pub_bytes+signature + proof = RNS.Packet(self, proof_data, packet_type=RNS.Packet.PROOF, context=RNS.Packet.LRPROOF) + 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): + if self.initiator: + peer_pub_bytes = packet.data[:Link.ECPUBSIZE] + signed_data = self.link_id+peer_pub_bytes + signature = packet.data[Link.ECPUBSIZE:RNS.Identity.KEYSIZE//8+Link.ECPUBSIZE] + + if self.destination.identity.validate(signature, signed_data): + self.loadPeer(peer_pub_bytes) + self.handshake() + self.rtt = time.time() - self.request_time + self.attached_interface = packet.receiving_interface + RNS.Transport.activateLink(self) + RNS.log("Link "+str(self)+" established with "+str(self.destination)+", RTT is "+str(self.rtt), RNS.LOG_VERBOSE) + rtt_data = umsgpack.packb(self.rtt) + rtt_packet = RNS.Packet(self, rtt_data, context=RNS.Packet.LRRTT) + RNS.log("Sending RTT packet", RNS.LOG_EXTREME); + rtt_packet.send() + + self.status = Link.ACTIVE + if self.callbacks.link_established != None: + thread = threading.Thread(target=self.callbacks.link_established, args=(self,)) + thread.setDaemon(True) + thread.start() + else: + RNS.log("Invalid link proof signature received by "+str(self), RNS.LOG_VERBOSE) + # TODO: should we really do this, or just wait + # for a valid one? Needs analysis. + self.teardown() + + + def rtt_packet(self, packet): + try: + # TODO: This is crude, we should use the delta + # to model a more representative per-bit round + # trip time, and use that to set a sensible RTT + # expectancy for the link. This will have to do + # for now though. + measured_rtt = time.time() - self.request_time + plaintext = self.decrypt(packet.data) + rtt = umsgpack.unpackb(plaintext) + self.rtt = max(measured_rtt, rtt) + self.status = Link.ACTIVE + + if self.owner.callbacks.link_established != None: + self.owner.callbacks.link_established(self) + except Exception as e: + RNS.log("Error occurred while processing RTT packet, tearing down link", RNS.LOG_ERROR) + traceback.print_exc() + self.teardown() + + def getSalt(self): + return self.link_id + + def getContext(self): + return None + + def teardown(self): + 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() + 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 + 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() + + self.prv = None + self.pub = None + self.pub_bytes = None + self.shared_key = None + self.derived_key = None + + if self.callbacks.link_closed != None: + self.callbacks.link_closed(self) + + def start_watchdog(self): + thread = threading.Thread(target=self.__watchdog_job) + thread.setDaemon(True) + thread.start() + + def __watchdog_job(self): + while not self.status == Link.CLOSED: + while (self.watchdog_lock): + sleep(max(self.rtt, 0.025)) + + if not self.status == Link.CLOSED: + # Link was initiated, but no response + # from destination yet + if self.status == Link.PENDING: + next_check = self.request_time + self.proof_timeout + sleep_time = next_check - time.time() + if time.time() >= self.request_time + self.proof_timeout: + 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: + next_check = self.request_time + self.proof_timeout + sleep_time = next_check - time.time() + if time.time() >= self.request_time + self.proof_timeout: + RNS.log("Timeout waiting for RTT packet from link initiator", RNS.LOG_DEBUG) + self.status = Link.CLOSED + self.teardown_reason = Link.TIMEOUT + self.link_closed() + sleep_time = 0.001 + + elif self.status == Link.ACTIVE: + if time.time() >= self.last_inbound + self.keepalive: + sleep_time = self.rtt * self.timeout_factor + Link.STALE_GRACE + self.status = Link.STALE + if self.initiator: + self.send_keepalive() + else: + sleep_time = (self.last_inbound + self.keepalive) - time.time() + + 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() + + sleep(sleep_time) + + + def send_keepalive(self): + keepalive_packet = RNS.Packet(self, bytes([0xFF]), context=RNS.Packet.KEEPALIVE) + keepalive_packet.send() + + 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: + RNS.log("Link-associated packet received on unexpected interface! Someone might be trying to manipulate your communication!", RNS.LOG_ERROR) + else: + self.last_inbound = time.time() + self.rx += 1 + self.rxbytes += len(packet.data) + if self.status == Link.STALE: + self.status = Link.ACTIVE + + if packet.packet_type == RNS.Packet.DATA: + if packet.context == RNS.Packet.NONE: + plaintext = self.decrypt(packet.data) + if self.callbacks.packet != None: + thread = threading.Thread(target=self.callbacks.packet, args=(plaintext, packet)) + thread.setDaemon(True) + thread.start() + + 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: + if not self.initiator: + self.rtt_packet(packet) + + elif packet.context == RNS.Packet.LINKCLOSE: + self.teardown_packet(packet) + + elif packet.context == RNS.Packet.RESOURCE_ADV: + packet.plaintext = self.decrypt(packet.data) + if self.resource_strategy == Link.ACCEPT_NONE: + pass + elif self.resource_strategy == Link.ACCEPT_APP: + if self.callbacks.resource != None: + thread = threading.Thread(target=self.callbacks.resource, args=(packet)) + thread.setDaemon(True) + thread.start() + elif self.resource_strategy == Link.ACCEPT_ALL: + RNS.Resource.accept(packet, self.callbacks.resource_concluded) + + elif packet.context == RNS.Packet.RESOURCE_REQ: + plaintext = self.decrypt(packet.data) + 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: + resource.request(plaintext) + + elif packet.context == RNS.Packet.RESOURCE_HMU: + plaintext = self.decrypt(packet.data) + resource_hash = plaintext[:RNS.Identity.HASHLENGTH//8] + for resource in self.incoming_resources: + if resource_hash == resource.hash: + resource.hashmap_update_packet(plaintext) + + elif packet.context == RNS.Packet.RESOURCE_ICL: + plaintext = self.decrypt(packet.data) + resource_hash = plaintext[:RNS.Identity.HASHLENGTH//8] + for resource in self.incoming_resources: + if resource_hash == resource.hash: + resource.cancel() + + 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() + + + # 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) + + 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: + resource.validateProof(packet.data) + + self.watchdog_lock = False + + + def encrypt(self, plaintext): + if self.__encryption_disabled: + return plaintext + try: + # TODO: Optimise this re-allocation + fernet = Fernet(base64.urlsafe_b64encode(self.derived_key)) + ciphertext = base64.urlsafe_b64decode(fernet.encrypt(plaintext)) + return ciphertext + except Exception as e: + RNS.log("Encryption on link "+str(self)+" failed. The contained exception was: "+str(e), RNS.LOG_ERROR) + + + def decrypt(self, ciphertext): + if self.__encryption_disabled: + return ciphertext + try: + fernet = Fernet(base64.urlsafe_b64encode(self.derived_key)) + plaintext = fernet.decrypt(base64.urlsafe_b64encode(ciphertext)) + return plaintext + except Exception as e: + RNS.log("Decryption failed on link "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR) + traceback.print_exc() + + 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): + self.callbacks.link_established = callback + + def link_closed_callback(self, callback): + self.callbacks.link_closed = callback + + def packet_callback(self, callback): + self.callbacks.packet = callback + + # Called when an incoming resource transfer is started + def resource_started_callback(self, callback): + self.callbacks.resource_started = callback + + # Called when a resource transfer is concluded + def resource_concluded_callback(self, callback): + self.callbacks.resource_concluded = callback + + 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: + 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) + + 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 disableEncryption(self): + if (RNS.Reticulum.should_allow_unencrypted()): + RNS.log("The link "+str(self)+" was downgraded to an encryptionless link", RNS.LOG_NOTICE) + self.__encryption_disabled = True + else: + RNS.log("Attempt to disable encryption on link, but encryptionless links are not allowed by config.", RNS.LOG_CRITICAL) + RNS.log("Shutting down Reticulum now!", RNS.LOG_CRITICAL) + RNS.panic() + + def encryption_disabled(self): + return self.__encryption_disabled + + def __str__(self): + return RNS.prettyhexrep(self.link_id) \ No newline at end of file diff --git a/RNS/Packet.py b/RNS/Packet.py index 42f56a1..9acb46f 100755 --- a/RNS/Packet.py +++ b/RNS/Packet.py @@ -5,411 +5,411 @@ import time import RNS class Packet: - # Packet types - DATA = 0x00 # Data packets - ANNOUNCE = 0x01 # Announces - LINKREQUEST = 0x02 # Link requests - PROOF = 0x03 # Proofs - types = [DATA, ANNOUNCE, LINKREQUEST, PROOF] + # Packet types + DATA = 0x00 # Data packets + ANNOUNCE = 0x01 # Announces + LINKREQUEST = 0x02 # Link requests + PROOF = 0x03 # Proofs + types = [DATA, ANNOUNCE, LINKREQUEST, PROOF] - # Header types - HEADER_1 = 0x00 # Normal header format - HEADER_2 = 0x01 # Header format used for packets in transport - HEADER_3 = 0x02 # Reserved - HEADER_4 = 0x03 # Reserved - header_types = [HEADER_1, HEADER_2, HEADER_3, HEADER_4] + # Header types + HEADER_1 = 0x00 # Normal header format + HEADER_2 = 0x01 # Header format used for packets in transport + HEADER_3 = 0x02 # Reserved + HEADER_4 = 0x03 # Reserved + header_types = [HEADER_1, HEADER_2, HEADER_3, HEADER_4] - # Data packet context types - NONE = 0x00 # Generic data packet - RESOURCE = 0x01 # Packet is part of a resource - RESOURCE_ADV = 0x02 # Packet is a resource advertisement - RESOURCE_REQ = 0x03 # Packet is a resource part request - RESOURCE_HMU = 0x04 # Packet is a resource hashmap update - RESOURCE_PRF = 0x05 # Packet is a resource proof - RESOURCE_ICL = 0x06 # Packet is a resource initiator cancel message - RESOURCE_RCL = 0x07 # Packet is a resource receiver cancel message - CACHE_REQUEST = 0x08 # Packet is a cache request - REQUEST = 0x09 # Packet is a request - RESPONSE = 0x0A # Packet is a response to a request - PATH_RESPONSE = 0x0B # Packet is a response to a path request - COMMAND = 0x0C # Packet is a command - COMMAND_STATUS = 0x0D # Packet is a status of an executed command - KEEPALIVE = 0xFB # Packet is a keepalive packet - 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 - LRPROOF = 0xFF # Packet is a link request proof + # Data packet context types + NONE = 0x00 # Generic data packet + RESOURCE = 0x01 # Packet is part of a resource + RESOURCE_ADV = 0x02 # Packet is a resource advertisement + RESOURCE_REQ = 0x03 # Packet is a resource part request + RESOURCE_HMU = 0x04 # Packet is a resource hashmap update + RESOURCE_PRF = 0x05 # Packet is a resource proof + RESOURCE_ICL = 0x06 # Packet is a resource initiator cancel message + RESOURCE_RCL = 0x07 # Packet is a resource receiver cancel message + CACHE_REQUEST = 0x08 # Packet is a cache request + REQUEST = 0x09 # Packet is a request + RESPONSE = 0x0A # Packet is a response to a request + PATH_RESPONSE = 0x0B # Packet is a response to a path request + COMMAND = 0x0C # Packet is a command + COMMAND_STATUS = 0x0D # Packet is a status of an executed command + KEEPALIVE = 0xFB # Packet is a keepalive packet + 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 + LRPROOF = 0xFF # Packet is a link request proof - # This is used to calculate allowable - # payload sizes - HEADER_MAXSIZE = 23 - MDU = RNS.Reticulum.MDU + # This is used to calculate allowable + # payload sizes + HEADER_MAXSIZE = 23 + MDU = RNS.Reticulum.MDU - # With an MTU of 500, the maximum RSA-encrypted - # amount of data we can send in a single packet - # is given by the below calculation; 258 bytes. - RSA_MDU = math.floor(MDU/RNS.Identity.DECRYPT_CHUNKSIZE)*RNS.Identity.ENCRYPT_CHUNKSIZE - PLAIN_MDU = MDU + # With an MTU of 500, the maximum RSA-encrypted + # amount of data we can send in a single packet + # is given by the below calculation; 258 bytes. + RSA_MDU = math.floor(MDU/RNS.Identity.DECRYPT_CHUNKSIZE)*RNS.Identity.ENCRYPT_CHUNKSIZE + PLAIN_MDU = MDU - # TODO: This should be calculated - # more intelligently - # Default packet timeout - TIMEOUT = 60 + # TODO: This should be calculated + # more intelligently + # Default packet timeout + TIMEOUT = 60 - def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST, header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True): - if destination != None: - if transport_type == None: - transport_type = RNS.Transport.BROADCAST + def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST, header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True): + if destination != None: + if transport_type == None: + transport_type = RNS.Transport.BROADCAST - self.header_type = header_type - self.packet_type = packet_type - self.transport_type = transport_type - self.context = context + self.header_type = header_type + self.packet_type = packet_type + self.transport_type = transport_type + self.context = context - self.hops = 0; - self.destination = destination - self.transport_id = transport_id - self.data = data - self.flags = self.getPackedFlags() + self.hops = 0; + self.destination = destination + self.transport_id = transport_id + self.data = data + self.flags = self.getPackedFlags() - self.raw = None - self.packed = False - self.sent = False - self.create_receipt = create_receipt - self.receipt = None - self.fromPacked = False - else: - self.raw = data - self.packed = True - self.fromPacked = True - self.create_receipt = False + self.raw = None + self.packed = False + self.sent = False + self.create_receipt = create_receipt + self.receipt = None + self.fromPacked = False + else: + self.raw = data + self.packed = True + self.fromPacked = True + self.create_receipt = False - self.MTU = RNS.Reticulum.MTU - self.sent_at = None - self.packet_hash = None + self.MTU = RNS.Reticulum.MTU + self.sent_at = None + self.packet_hash = None - self.attached_interface = attached_interface - self.receiving_interface = None + self.attached_interface = attached_interface + self.receiving_interface = None - def getPackedFlags(self): - if self.context == Packet.LRPROOF: - packed_flags = (self.header_type << 6) | (self.transport_type << 4) | RNS.Destination.LINK | self.packet_type - else: - packed_flags = (self.header_type << 6) | (self.transport_type << 4) | (self.destination.type << 2) | self.packet_type - return packed_flags + def getPackedFlags(self): + if self.context == Packet.LRPROOF: + packed_flags = (self.header_type << 6) | (self.transport_type << 4) | RNS.Destination.LINK | self.packet_type + else: + packed_flags = (self.header_type << 6) | (self.transport_type << 4) | (self.destination.type << 2) | self.packet_type + return packed_flags - def pack(self): - self.destination_hash = self.destination.hash - self.header = b"" - self.header += struct.pack("!B", self.flags) - self.header += struct.pack("!B", self.hops) + def pack(self): + self.destination_hash = self.destination.hash + self.header = b"" + self.header += struct.pack("!B", self.flags) + self.header += struct.pack("!B", self.hops) - if self.context == Packet.LRPROOF: - self.header += self.destination.link_id - self.ciphertext = self.data - else: - if self.header_type == Packet.HEADER_1: - self.header += self.destination.hash + if self.context == Packet.LRPROOF: + self.header += self.destination.link_id + self.ciphertext = self.data + else: + if self.header_type == Packet.HEADER_1: + self.header += self.destination.hash - if self.packet_type == Packet.ANNOUNCE: - # Announce packets are not encrypted - self.ciphertext = self.data - elif self.packet_type == Packet.PROOF and self.context == Packet.RESOURCE_PRF: - # Resource proofs are not encrypted - 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: - # A resource takes care of symmetric - # encryption by itself - self.ciphertext = self.data - elif self.context == Packet.KEEPALIVE: - # Keepalive packets contain no actual - # data - self.ciphertext = self.data - elif self.context == Packet.CACHE_REQUEST: - # Cache-requests are not encrypted - self.ciphertext = self.data - else: - # In all other cases, we encrypt the packet - # with the destination's encryption method - self.ciphertext = self.destination.encrypt(self.data) + if self.packet_type == Packet.ANNOUNCE: + # Announce packets are not encrypted + self.ciphertext = self.data + elif self.packet_type == Packet.PROOF and self.context == Packet.RESOURCE_PRF: + # Resource proofs are not encrypted + 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: + # A resource takes care of symmetric + # encryption by itself + self.ciphertext = self.data + elif self.context == Packet.KEEPALIVE: + # Keepalive packets contain no actual + # data + self.ciphertext = self.data + elif self.context == Packet.CACHE_REQUEST: + # Cache-requests are not encrypted + self.ciphertext = self.data + else: + # In all other cases, we encrypt the packet + # with the destination's encryption method + self.ciphertext = self.destination.encrypt(self.data) - if self.header_type == Packet.HEADER_2: - if self.transport_id != None: - self.header += self.transport_id - self.header += self.destination.hash + if self.header_type == Packet.HEADER_2: + if self.transport_id != None: + self.header += self.transport_id + self.header += self.destination.hash - if self.packet_type == Packet.ANNOUNCE: - # Announce packets are not encrypted - self.ciphertext = self.data - else: - raise IOError("Packet with header type 2 must have a transport ID") + if self.packet_type == Packet.ANNOUNCE: + # Announce packets are not encrypted + self.ciphertext = self.data + else: + raise IOError("Packet with header type 2 must have a transport ID") - self.header += bytes([self.context]) - self.raw = self.header + self.ciphertext + self.header += bytes([self.context]) + self.raw = self.header + self.ciphertext - if len(self.raw) > self.MTU: - raise IOError("Packet size of "+str(len(self.raw))+" exceeds MTU of "+str(self.MTU)+" bytes") + if len(self.raw) > self.MTU: + raise IOError("Packet size of "+str(len(self.raw))+" exceeds MTU of "+str(self.MTU)+" bytes") - self.packed = True - self.updateHash() + self.packed = True + self.updateHash() - def unpack(self): - self.flags = self.raw[0] - self.hops = self.raw[1] + def unpack(self): + self.flags = self.raw[0] + self.hops = self.raw[1] - self.header_type = (self.flags & 0b11000000) >> 6 - self.transport_type = (self.flags & 0b00110000) >> 4 - self.destination_type = (self.flags & 0b00001100) >> 2 - self.packet_type = (self.flags & 0b00000011) + self.header_type = (self.flags & 0b11000000) >> 6 + self.transport_type = (self.flags & 0b00110000) >> 4 + self.destination_type = (self.flags & 0b00001100) >> 2 + self.packet_type = (self.flags & 0b00000011) - if self.header_type == Packet.HEADER_2: - self.transport_id = self.raw[2:12] - self.destination_hash = self.raw[12:22] - self.context = ord(self.raw[22:23]) - self.data = self.raw[23:] - else: - self.transport_id = None - self.destination_hash = self.raw[2:12] - self.context = ord(self.raw[12:13]) - self.data = self.raw[13:] + if self.header_type == Packet.HEADER_2: + self.transport_id = self.raw[2:12] + self.destination_hash = self.raw[12:22] + self.context = ord(self.raw[22:23]) + self.data = self.raw[23:] + else: + self.transport_id = None + self.destination_hash = self.raw[2:12] + self.context = ord(self.raw[12:13]) + self.data = self.raw[13:] - self.packed = False - self.updateHash() + self.packed = False + self.updateHash() - # Sends the packet. Returns a receipt if one is generated, - # or None if no receipt is available. Returns False if the - # packet could not be sent. - def send(self): - if not self.sent: - if self.destination.type == RNS.Destination.LINK: - if self.destination.status == RNS.Link.CLOSED: - raise IOError("Attempt to transmit over a closed link") - else: - self.destination.last_outbound = time.time() - self.destination.tx += 1 - self.destination.txbytes += len(self.data) + # Sends the packet. Returns a receipt if one is generated, + # or None if no receipt is available. Returns False if the + # packet could not be sent. + def send(self): + if not self.sent: + if self.destination.type == RNS.Destination.LINK: + if self.destination.status == RNS.Link.CLOSED: + raise IOError("Attempt to transmit over a closed link") + else: + self.destination.last_outbound = time.time() + self.destination.tx += 1 + self.destination.txbytes += len(self.data) - if not self.packed: - self.pack() - - if RNS.Transport.outbound(self): - return self.receipt - else: - RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR) - self.sent = False - self.receipt = None - return False - - else: - raise IOError("Packet was already sent") + if not self.packed: + self.pack() + + if RNS.Transport.outbound(self): + return self.receipt + else: + RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR) + self.sent = False + self.receipt = None + return False + + else: + raise IOError("Packet was already sent") - def resend(self): - if self.sent: - if RNS.Transport.outbound(self): - return self.receipt - else: - RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR) - self.sent = False - self.receipt = None - return False - else: - raise IOError("Packet was not sent yet") + def resend(self): + if self.sent: + if RNS.Transport.outbound(self): + return self.receipt + else: + RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR) + self.sent = False + self.receipt = None + return False + else: + raise IOError("Packet was not sent yet") - def prove(self, destination=None): - if self.fromPacked and hasattr(self, "destination") and self.destination: - if self.destination.identity and self.destination.identity.prv: - 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) + def prove(self, destination=None): + if self.fromPacked and hasattr(self, "destination") and self.destination: + if self.destination.identity and self.destination.identity.prv: + 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 - # to direct the proof back to the proved packet's sender - def generateProofDestination(self): - return ProofDestination(self) + # Generates a special destination that allows Reticulum + # to direct the proof back to the proved packet's sender + def generateProofDestination(self): + return ProofDestination(self) - def validateProofPacket(self, proof_packet): - return self.receipt.validateProofPacket(proof_packet) + def validateProofPacket(self, proof_packet): + return self.receipt.validateProofPacket(proof_packet) - def validateProof(self, proof): - return self.receipt.validateProof(proof) + def validateProof(self, proof): + return self.receipt.validateProof(proof) - def updateHash(self): - self.packet_hash = self.getHash() + def updateHash(self): + self.packet_hash = self.getHash() - def getHash(self): - return RNS.Identity.fullHash(self.getHashablePart()) + def getHash(self): + return RNS.Identity.fullHash(self.getHashablePart()) - def getTruncatedHash(self): - return RNS.Identity.truncatedHash(self.getHashablePart()) + def getTruncatedHash(self): + return RNS.Identity.truncatedHash(self.getHashablePart()) - def getHashablePart(self): - hashable_part = bytes([self.raw[0] & 0b00001111]) - if self.header_type == Packet.HEADER_2: - hashable_part += self.raw[12:] - else: - hashable_part += self.raw[2:] + def getHashablePart(self): + hashable_part = bytes([self.raw[0] & 0b00001111]) + if self.header_type == Packet.HEADER_2: + hashable_part += self.raw[12:] + else: + hashable_part += self.raw[2:] - return hashable_part + return hashable_part class ProofDestination: - def __init__(self, packet): - self.hash = packet.getHash()[:10]; - self.type = RNS.Destination.SINGLE + def __init__(self, packet): + self.hash = packet.getHash()[:10]; + self.type = RNS.Destination.SINGLE - def encrypt(self, plaintext): - return plaintext + def encrypt(self, plaintext): + return plaintext class PacketReceipt: - # Receipt status constants - FAILED = 0x00 - SENT = 0x01 - DELIVERED = 0x02 - CULLED = 0xFF + # Receipt status constants + FAILED = 0x00 + SENT = 0x01 + DELIVERED = 0x02 + CULLED = 0xFF - EXPL_LENGTH = RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8 - IMPL_LENGTH = RNS.Identity.SIGLENGTH//8 + EXPL_LENGTH = RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8 + IMPL_LENGTH = RNS.Identity.SIGLENGTH//8 - # Creates a new packet receipt from a sent packet - def __init__(self, packet): - self.hash = packet.getHash() - self.sent = True - self.sent_at = time.time() - self.timeout = Packet.TIMEOUT - self.proved = False - self.status = PacketReceipt.SENT - self.destination = packet.destination - self.callbacks = PacketReceiptCallbacks() - self.concluded_at = None + # Creates a new packet receipt from a sent packet + def __init__(self, packet): + self.hash = packet.getHash() + self.sent = True + self.sent_at = time.time() + self.timeout = Packet.TIMEOUT + self.proved = False + self.status = PacketReceipt.SENT + self.destination = packet.destination + self.callbacks = PacketReceiptCallbacks() + self.concluded_at = None - # Validate a proof packet - def validateProofPacket(self, proof_packet): - 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 proof packet + def validateProofPacket(self, proof_packet): + 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 - # TODO: Why is this disabled? - # 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 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 + # TODO: Why is this disabled? + # 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 - def validateProof(self, proof): - if 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 = self.destination.identity.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: - # This is an implicit proof - if self.destination.identity == None: - return False + # Validate a raw proof + def validateProof(self, proof): + if 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 = self.destination.identity.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: + # This is an implicit proof + if self.destination.identity == None: + return False - signature = proof[:RNS.Identity.SIGLENGTH//8] - proof_valid = self.destination.identity.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 + signature = proof[:RNS.Identity.SIGLENGTH//8] + proof_valid = self.destination.identity.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 - def rtt(self): - return self.concluded_at - self.sent_at + def rtt(self): + return self.concluded_at - self.sent_at - def is_timed_out(self): - return (self.sent_at+self.timeout < time.time()) + def is_timed_out(self): + return (self.sent_at+self.timeout < time.time()) - def check_timeout(self): - if self.is_timed_out(): - if self.timeout == -1: - self.status = PacketReceipt.CULLED - else: - self.status = PacketReceipt.FAILED + def check_timeout(self): + if self.is_timed_out(): + if self.timeout == -1: + self.status = PacketReceipt.CULLED + else: + self.status = PacketReceipt.FAILED - self.concluded_at = time.time() + self.concluded_at = time.time() - if self.callbacks.timeout: - thread = threading.Thread(target=self.callbacks.timeout, args=(self,)) - thread.setDaemon(True) - thread.start() - #self.callbacks.timeout(self) + if self.callbacks.timeout: + thread = threading.Thread(target=self.callbacks.timeout, args=(self,)) + thread.setDaemon(True) + thread.start() + #self.callbacks.timeout(self) - # Set the timeout in seconds - def set_timeout(self, timeout): - self.timeout = float(timeout) + # Set the timeout in seconds + def set_timeout(self, timeout): + self.timeout = float(timeout) - # Set a function that gets called when - # a successfull delivery has been proved - def delivery_callback(self, callback): - self.callbacks.delivery = callback + # Set a function that gets called when + # a successfull delivery has been proved + def delivery_callback(self, callback): + self.callbacks.delivery = callback - # Set a function that gets called if the - # delivery times out - def timeout_callback(self, callback): - self.callbacks.timeout = callback + # Set a function that gets called if the + # delivery times out + def timeout_callback(self, callback): + self.callbacks.timeout = callback class PacketReceiptCallbacks: - def __init__(self): - self.delivery = None - self.timeout = None \ No newline at end of file + def __init__(self): + self.delivery = None + self.timeout = None \ No newline at end of file diff --git a/RNS/Reticulum.py b/RNS/Reticulum.py index 445f670..60544c2 100755 --- a/RNS/Reticulum.py +++ b/RNS/Reticulum.py @@ -10,387 +10,387 @@ import os import RNS class Reticulum: - MTU = 500 - HEADER_MAXSIZE = 23 - MDU = MTU - HEADER_MAXSIZE + MTU = 500 + HEADER_MAXSIZE = 23 + MDU = MTU - HEADER_MAXSIZE - router = None - config = None - - configdir = os.path.expanduser("~")+"/.reticulum" - configpath = "" - storagepath = "" - cachepath = "" - - @staticmethod - def exit_handler(): - RNS.Transport.exitHandler() - RNS.Identity.exitHandler() + router = None + config = None + + configdir = os.path.expanduser("~")+"/.reticulum" + configpath = "" + storagepath = "" + cachepath = "" + + @staticmethod + def exit_handler(): + RNS.Transport.exitHandler() + RNS.Identity.exitHandler() - def __init__(self,configdir=None): - if configdir != None: - Reticulum.configdir = configdir - - Reticulum.configpath = Reticulum.configdir+"/config" - Reticulum.storagepath = Reticulum.configdir+"/storage" - Reticulum.cachepath = Reticulum.configdir+"/storage/cache" - Reticulum.resourcepath = Reticulum.configdir+"/storage/resources" + def __init__(self,configdir=None): + if configdir != None: + Reticulum.configdir = configdir + + Reticulum.configpath = Reticulum.configdir+"/config" + Reticulum.storagepath = Reticulum.configdir+"/storage" + Reticulum.cachepath = Reticulum.configdir+"/storage/cache" + Reticulum.resourcepath = Reticulum.configdir+"/storage/resources" - Reticulum.__allow_unencrypted = False - Reticulum.__transport_enabled = False - Reticulum.__use_implicit_proof = True + Reticulum.__allow_unencrypted = False + Reticulum.__transport_enabled = False + Reticulum.__use_implicit_proof = True - self.local_interface_port = 37428 - self.share_instance = True + self.local_interface_port = 37428 + self.share_instance = True - self.is_shared_instance = False - self.is_connected_to_shared_instance = False - self.is_standalone_instance = False + self.is_shared_instance = False + self.is_connected_to_shared_instance = False + self.is_standalone_instance = False - if not os.path.isdir(Reticulum.storagepath): - os.makedirs(Reticulum.storagepath) + if not os.path.isdir(Reticulum.storagepath): + os.makedirs(Reticulum.storagepath) - if not os.path.isdir(Reticulum.cachepath): - os.makedirs(Reticulum.cachepath) + if not os.path.isdir(Reticulum.cachepath): + os.makedirs(Reticulum.cachepath) - if not os.path.isdir(Reticulum.resourcepath): - os.makedirs(Reticulum.resourcepath) + if not os.path.isdir(Reticulum.resourcepath): + os.makedirs(Reticulum.resourcepath) - if os.path.isfile(self.configpath): - try: - self.config = ConfigObj(self.configpath) - RNS.log("Configuration loaded from "+self.configpath) - except Exception as e: - RNS.log("Could not parse the configuration at "+self.configpath, RNS.LOG_ERROR) - RNS.log("Check your configuration file for errors!", RNS.LOG_ERROR) - RNS.panic() - else: - RNS.log("Could not load config file, creating default configuration file...") - self.createDefaultConfig() - RNS.log("Default config file created. Make any necessary changes in "+Reticulum.configdir+"/config and start Reticulum again.") - RNS.log("Exiting now!") - exit(1) + if os.path.isfile(self.configpath): + try: + self.config = ConfigObj(self.configpath) + RNS.log("Configuration loaded from "+self.configpath) + except Exception as e: + RNS.log("Could not parse the configuration at "+self.configpath, RNS.LOG_ERROR) + RNS.log("Check your configuration file for errors!", RNS.LOG_ERROR) + RNS.panic() + else: + RNS.log("Could not load config file, creating default configuration file...") + self.createDefaultConfig() + RNS.log("Default config file created. Make any necessary changes in "+Reticulum.configdir+"/config and start Reticulum again.") + RNS.log("Exiting now!") + exit(1) - self.applyConfig() - RNS.Identity.loadKnownDestinations() + self.applyConfig() + RNS.Identity.loadKnownDestinations() - RNS.Transport.start(self) + RNS.Transport.start(self) - atexit.register(Reticulum.exit_handler) + atexit.register(Reticulum.exit_handler) - def start_local_interface(self): - if self.share_instance: - try: - interface = LocalInterface.LocalServerInterface( - RNS.Transport, - self.local_interface_port - ) - interface.OUT = True - RNS.Transport.interfaces.append(interface) - self.is_shared_instance = True - RNS.log("Started shared instance interface: "+str(interface), RNS.LOG_DEBUG) - except Exception as e: - try: - interface = LocalInterface.LocalClientInterface( - RNS.Transport, - "Local shared instance", - self.local_interface_port) - interface.target_port = self.local_interface_port - interface.OUT = True - RNS.Transport.interfaces.append(interface) - self.is_shared_instance = False - self.is_standalone_instance = False - self.is_connected_to_shared_instance = True - RNS.log("Connected to local shared instance via: "+str(interface), RNS.LOG_DEBUG) - except Exception as e: - RNS.log("Local shared instance appears to be running, but it could not be connected", RNS.LOG_ERROR) - RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) - self.is_shared_instance = False - self.is_standalone_instance = True - self.is_connected_to_shared_instance = False - else: - self.is_shared_instance = False - self.is_standalone_instance = True - self.is_connected_to_shared_instance = False + def start_local_interface(self): + if self.share_instance: + try: + interface = LocalInterface.LocalServerInterface( + RNS.Transport, + self.local_interface_port + ) + interface.OUT = True + RNS.Transport.interfaces.append(interface) + self.is_shared_instance = True + RNS.log("Started shared instance interface: "+str(interface), RNS.LOG_DEBUG) + except Exception as e: + try: + interface = LocalInterface.LocalClientInterface( + RNS.Transport, + "Local shared instance", + self.local_interface_port) + interface.target_port = self.local_interface_port + interface.OUT = True + RNS.Transport.interfaces.append(interface) + self.is_shared_instance = False + self.is_standalone_instance = False + self.is_connected_to_shared_instance = True + RNS.log("Connected to local shared instance via: "+str(interface), RNS.LOG_DEBUG) + except Exception as e: + RNS.log("Local shared instance appears to be running, but it could not be connected", RNS.LOG_ERROR) + RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) + self.is_shared_instance = False + self.is_standalone_instance = True + self.is_connected_to_shared_instance = False + else: + self.is_shared_instance = False + self.is_standalone_instance = True + self.is_connected_to_shared_instance = False - def applyConfig(self): - if "logging" in self.config: - for option in self.config["logging"]: - value = self.config["logging"][option] - if option == "loglevel": - RNS.loglevel = int(value) - if RNS.loglevel < 0: - RNS.loglevel = 0 - if RNS.loglevel > 7: - RNS.loglevel = 7 + def applyConfig(self): + if "logging" in self.config: + for option in self.config["logging"]: + value = self.config["logging"][option] + if option == "loglevel": + RNS.loglevel = int(value) + if RNS.loglevel < 0: + RNS.loglevel = 0 + if RNS.loglevel > 7: + RNS.loglevel = 7 - if "reticulum" in self.config: - for option in self.config["reticulum"]: - value = self.config["reticulum"][option] - if option == "share_instance": - value = self.config["reticulum"].as_bool(option) - self.share_instance = value - if option == "shared_instance_port": - value = int(self.config["reticulum"][option]) - self.local_interface_port = value - if option == "enable_transport": - v = self.config["reticulum"].as_bool(option) - if v == True: - Reticulum.__transport_enabled = True - if option == "use_implicit_proof": - v = self.config["reticulum"].as_bool(option) - if v == True: - Reticulum.__use_implicit_proof = True - if v == False: - Reticulum.__use_implicit_proof = False - if option == "allow_unencrypted": - v = self.config["reticulum"].as_bool(option) - if v == True: - RNS.log("", RNS.LOG_CRITICAL) - RNS.log("! ! ! ! ! ! ! ! !", RNS.LOG_CRITICAL) - RNS.log("", RNS.LOG_CRITICAL) - RNS.log("Danger! Encryptionless links have been allowed in the config file!", RNS.LOG_CRITICAL) - RNS.log("Beware of the consequences! Any data sent over a link can potentially be intercepted,", RNS.LOG_CRITICAL) - RNS.log("read and modified! If you are not absolutely sure that you want this,", RNS.LOG_CRITICAL) - RNS.log("you should exit Reticulum NOW and change your config file!", RNS.LOG_CRITICAL) - RNS.log("", RNS.LOG_CRITICAL) - RNS.log("! ! ! ! ! ! ! ! !", RNS.LOG_CRITICAL) - RNS.log("", RNS.LOG_CRITICAL) - Reticulum.__allow_unencrypted = True + if "reticulum" in self.config: + for option in self.config["reticulum"]: + value = self.config["reticulum"][option] + if option == "share_instance": + value = self.config["reticulum"].as_bool(option) + self.share_instance = value + if option == "shared_instance_port": + value = int(self.config["reticulum"][option]) + self.local_interface_port = value + if option == "enable_transport": + v = self.config["reticulum"].as_bool(option) + if v == True: + Reticulum.__transport_enabled = True + if option == "use_implicit_proof": + v = self.config["reticulum"].as_bool(option) + if v == True: + Reticulum.__use_implicit_proof = True + if v == False: + Reticulum.__use_implicit_proof = False + if option == "allow_unencrypted": + v = self.config["reticulum"].as_bool(option) + if v == True: + RNS.log("", RNS.LOG_CRITICAL) + RNS.log("! ! ! ! ! ! ! ! !", RNS.LOG_CRITICAL) + RNS.log("", RNS.LOG_CRITICAL) + RNS.log("Danger! Encryptionless links have been allowed in the config file!", RNS.LOG_CRITICAL) + RNS.log("Beware of the consequences! Any data sent over a link can potentially be intercepted,", RNS.LOG_CRITICAL) + RNS.log("read and modified! If you are not absolutely sure that you want this,", RNS.LOG_CRITICAL) + RNS.log("you should exit Reticulum NOW and change your config file!", RNS.LOG_CRITICAL) + RNS.log("", RNS.LOG_CRITICAL) + RNS.log("! ! ! ! ! ! ! ! !", RNS.LOG_CRITICAL) + RNS.log("", RNS.LOG_CRITICAL) + Reticulum.__allow_unencrypted = True - self.start_local_interface() + self.start_local_interface() - if self.is_shared_instance or self.is_standalone_instance: - interface_names = [] - for name in self.config["interfaces"]: - if not name in interface_names: - c = self.config["interfaces"][name] + if self.is_shared_instance or self.is_standalone_instance: + interface_names = [] + for name in self.config["interfaces"]: + if not name in interface_names: + c = self.config["interfaces"][name] - try: - if ("interface_enabled" in c) and c.as_bool("interface_enabled") == True: - if c["type"] == "UdpInterface": - interface = UdpInterface.UdpInterface( - RNS.Transport, - name, - c["listen_ip"], - int(c["listen_port"]), - c["forward_ip"], - int(c["forward_port"]) - ) + try: + if ("interface_enabled" in c) and c.as_bool("interface_enabled") == True: + if c["type"] == "UdpInterface": + interface = UdpInterface.UdpInterface( + RNS.Transport, + name, + c["listen_ip"], + int(c["listen_port"]), + c["forward_ip"], + int(c["forward_port"]) + ) - if "outgoing" in c and c.as_bool("outgoing") == True: - interface.OUT = True - else: - interface.OUT = False + if "outgoing" in c and c.as_bool("outgoing") == True: + interface.OUT = True + else: + interface.OUT = False - RNS.Transport.interfaces.append(interface) + RNS.Transport.interfaces.append(interface) - if c["type"] == "TCPServerInterface": - interface = TCPInterface.TCPServerInterface( - RNS.Transport, - name, - c["listen_ip"], - int(c["listen_port"]) - ) + if c["type"] == "TCPServerInterface": + interface = TCPInterface.TCPServerInterface( + RNS.Transport, + name, + c["listen_ip"], + int(c["listen_port"]) + ) - if "outgoing" in c and c.as_bool("outgoing") == True: - interface.OUT = True - else: - interface.OUT = False + if "outgoing" in c and c.as_bool("outgoing") == True: + interface.OUT = True + else: + interface.OUT = False - RNS.Transport.interfaces.append(interface) + RNS.Transport.interfaces.append(interface) - if c["type"] == "TCPClientInterface": - interface = TCPInterface.TCPClientInterface( - RNS.Transport, - name, - c["target_host"], - int(c["target_port"]) - ) + if c["type"] == "TCPClientInterface": + interface = TCPInterface.TCPClientInterface( + RNS.Transport, + name, + c["target_host"], + int(c["target_port"]) + ) - if "outgoing" in c and c.as_bool("outgoing") == True: - interface.OUT = True - else: - interface.OUT = False + if "outgoing" in c and c.as_bool("outgoing") == True: + interface.OUT = True + else: + interface.OUT = False - RNS.Transport.interfaces.append(interface) + RNS.Transport.interfaces.append(interface) - if c["type"] == "SerialInterface": - port = c["port"] if "port" in c else None - speed = int(c["speed"]) if "speed" in c else 9600 - databits = int(c["databits"]) if "databits" in c else 8 - parity = c["parity"] if "parity" in c else "N" - stopbits = int(c["stopbits"]) if "stopbits" in c else 1 + if c["type"] == "SerialInterface": + port = c["port"] if "port" in c else None + speed = int(c["speed"]) if "speed" in c else 9600 + databits = int(c["databits"]) if "databits" in c else 8 + parity = c["parity"] if "parity" in c else "N" + stopbits = int(c["stopbits"]) if "stopbits" in c else 1 - if port == None: - raise ValueError("No port specified for serial interface") + if port == None: + raise ValueError("No port specified for serial interface") - interface = SerialInterface.SerialInterface( - RNS.Transport, - name, - port, - speed, - databits, - parity, - stopbits - ) + interface = SerialInterface.SerialInterface( + RNS.Transport, + name, + port, + speed, + databits, + parity, + stopbits + ) - if "outgoing" in c and c["outgoing"].lower() == "true": - interface.OUT = True - else: - interface.OUT = False + if "outgoing" in c and c["outgoing"].lower() == "true": + interface.OUT = True + else: + interface.OUT = False - RNS.Transport.interfaces.append(interface) + RNS.Transport.interfaces.append(interface) - if c["type"] == "KISSInterface": - preamble = int(c["preamble"]) if "preamble" in c else None - txtail = int(c["txtail"]) if "txtail" in c else None - persistence = int(c["persistence"]) if "persistence" in c else None - slottime = int(c["slottime"]) if "slottime" in c else None - flow_control = c.as_bool("flow_control") if "flow_control" in c else False - port = c["port"] if "port" in c else None - speed = int(c["speed"]) if "speed" in c else 9600 - databits = int(c["databits"]) if "databits" in c else 8 - parity = c["parity"] if "parity" in c else "N" - stopbits = int(c["stopbits"]) if "stopbits" in c else 1 + if c["type"] == "KISSInterface": + preamble = int(c["preamble"]) if "preamble" in c else None + txtail = int(c["txtail"]) if "txtail" in c else None + persistence = int(c["persistence"]) if "persistence" in c else None + slottime = int(c["slottime"]) if "slottime" in c else None + flow_control = c.as_bool("flow_control") if "flow_control" in c else False + port = c["port"] if "port" in c else None + speed = int(c["speed"]) if "speed" in c else 9600 + databits = int(c["databits"]) if "databits" in c else 8 + parity = c["parity"] if "parity" in c else "N" + stopbits = int(c["stopbits"]) if "stopbits" in c else 1 - if port == None: - raise ValueError("No port specified for serial interface") + if port == None: + raise ValueError("No port specified for serial interface") - interface = KISSInterface.KISSInterface( - RNS.Transport, - name, - port, - speed, - databits, - parity, - stopbits, - preamble, - txtail, - persistence, - slottime, - flow_control - ) + interface = KISSInterface.KISSInterface( + RNS.Transport, + name, + port, + speed, + databits, + parity, + stopbits, + preamble, + txtail, + persistence, + slottime, + flow_control + ) - if "outgoing" in c and c["outgoing"].lower() == "true": - interface.OUT = True - else: - interface.OUT = False + if "outgoing" in c and c["outgoing"].lower() == "true": + interface.OUT = True + else: + interface.OUT = False - RNS.Transport.interfaces.append(interface) + RNS.Transport.interfaces.append(interface) - if c["type"] == "AX25KISSInterface": - preamble = int(c["preamble"]) if "preamble" in c else None - txtail = int(c["txtail"]) if "txtail" in c else None - persistence = int(c["persistence"]) if "persistence" in c else None - slottime = int(c["slottime"]) if "slottime" in c else None - flow_control = c.as_bool("flow_control") if "flow_control" in c else False - port = c["port"] if "port" in c else None - speed = int(c["speed"]) if "speed" in c else 9600 - databits = int(c["databits"]) if "databits" in c else 8 - parity = c["parity"] if "parity" in c else "N" - stopbits = int(c["stopbits"]) if "stopbits" in c else 1 + if c["type"] == "AX25KISSInterface": + preamble = int(c["preamble"]) if "preamble" in c else None + txtail = int(c["txtail"]) if "txtail" in c else None + persistence = int(c["persistence"]) if "persistence" in c else None + slottime = int(c["slottime"]) if "slottime" in c else None + flow_control = c.as_bool("flow_control") if "flow_control" in c else False + port = c["port"] if "port" in c else None + speed = int(c["speed"]) if "speed" in c else 9600 + databits = int(c["databits"]) if "databits" in c else 8 + parity = c["parity"] if "parity" in c else "N" + stopbits = int(c["stopbits"]) if "stopbits" in c else 1 - callsign = c["callsign"] if "callsign" in c else "" - ssid = int(c["ssid"]) if "ssid" in c else -1 + callsign = c["callsign"] if "callsign" in c else "" + ssid = int(c["ssid"]) if "ssid" in c else -1 - if port == None: - raise ValueError("No port specified for serial interface") + if port == None: + raise ValueError("No port specified for serial interface") - interface = AX25KISSInterface.AX25KISSInterface( - RNS.Transport, - name, - callsign, - ssid, - port, - speed, - databits, - parity, - stopbits, - preamble, - txtail, - persistence, - slottime, - flow_control - ) + interface = AX25KISSInterface.AX25KISSInterface( + RNS.Transport, + name, + callsign, + ssid, + port, + speed, + databits, + parity, + stopbits, + preamble, + txtail, + persistence, + slottime, + flow_control + ) - if "outgoing" in c and c["outgoing"].lower() == "true": - interface.OUT = True - else: - interface.OUT = False + if "outgoing" in c and c["outgoing"].lower() == "true": + interface.OUT = True + else: + interface.OUT = False - RNS.Transport.interfaces.append(interface) + RNS.Transport.interfaces.append(interface) - if c["type"] == "RNodeInterface": - frequency = int(c["frequency"]) if "frequency" in c else None - bandwidth = int(c["bandwidth"]) if "bandwidth" in c else None - txpower = int(c["txpower"]) if "txpower" in c else None - spreadingfactor = int(c["spreadingfactor"]) if "spreadingfactor" in c else None - codingrate = int(c["codingrate"]) if "codingrate" in c else None - flow_control = c.as_bool("flow_control") if "flow_control" in c else False - id_interval = int(c["id_interval"]) if "id_interval" in c else None - id_callsign = c["id_callsign"] if "id_callsign" in c else None + if c["type"] == "RNodeInterface": + frequency = int(c["frequency"]) if "frequency" in c else None + bandwidth = int(c["bandwidth"]) if "bandwidth" in c else None + txpower = int(c["txpower"]) if "txpower" in c else None + spreadingfactor = int(c["spreadingfactor"]) if "spreadingfactor" in c else None + codingrate = int(c["codingrate"]) if "codingrate" in c else None + flow_control = c.as_bool("flow_control") if "flow_control" in c else False + id_interval = int(c["id_interval"]) if "id_interval" in c else None + id_callsign = c["id_callsign"] if "id_callsign" in c else None - port = c["port"] if "port" in c else None - - if port == None: - raise ValueError("No port specified for RNode interface") + port = c["port"] if "port" in c else None + + if port == None: + raise ValueError("No port specified for RNode interface") - interface = RNodeInterface.RNodeInterface( - RNS.Transport, - name, - port, - frequency = frequency, - bandwidth = bandwidth, - txpower = txpower, - sf = spreadingfactor, - cr = codingrate, - flow_control = flow_control, - id_interval = id_interval, - id_callsign = id_callsign - ) + interface = RNodeInterface.RNodeInterface( + RNS.Transport, + name, + port, + frequency = frequency, + bandwidth = bandwidth, + txpower = txpower, + sf = spreadingfactor, + cr = codingrate, + flow_control = flow_control, + id_interval = id_interval, + id_callsign = id_callsign + ) - if "outgoing" in c and c["outgoing"].lower() == "true": - interface.OUT = True - else: - interface.OUT = False + if "outgoing" in c and c["outgoing"].lower() == "true": + interface.OUT = True + else: + interface.OUT = False - RNS.Transport.interfaces.append(interface) - else: - RNS.log("Skipping disabled interface \""+name+"\"", RNS.LOG_NOTICE) + RNS.Transport.interfaces.append(interface) + else: + RNS.log("Skipping disabled interface \""+name+"\"", RNS.LOG_NOTICE) - except Exception as e: - RNS.log("The interface \""+name+"\" could not be created. Check your configuration file for errors!", RNS.LOG_ERROR) - RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) - RNS.panic() - else: - RNS.log("The interface name \""+name+"\" was already used. Check your configuration file for errors!", RNS.LOG_ERROR) - RNS.panic() - + except Exception as e: + RNS.log("The interface \""+name+"\" could not be created. Check your configuration file for errors!", RNS.LOG_ERROR) + RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) + RNS.panic() + else: + RNS.log("The interface name \""+name+"\" was already used. Check your configuration file for errors!", RNS.LOG_ERROR) + RNS.panic() + - def createDefaultConfig(self): - self.config = ConfigObj(__default_rns_config__) - self.config.filename = Reticulum.configpath - - if not os.path.isdir(Reticulum.configdir): - os.makedirs(Reticulum.configdir) - self.config.write() - self.applyConfig() + def createDefaultConfig(self): + self.config = ConfigObj(__default_rns_config__) + self.config.filename = Reticulum.configpath + + if not os.path.isdir(Reticulum.configdir): + os.makedirs(Reticulum.configdir) + self.config.write() + self.applyConfig() - @staticmethod - def should_allow_unencrypted(): - return Reticulum.__allow_unencrypted + @staticmethod + def should_allow_unencrypted(): + return Reticulum.__allow_unencrypted - @staticmethod - def should_use_implicit_proof(): - return Reticulum.__use_implicit_proof + @staticmethod + def should_use_implicit_proof(): + return Reticulum.__use_implicit_proof - @staticmethod - def transport_enabled(): - return Reticulum.__transport_enabled + @staticmethod + def transport_enabled(): + return Reticulum.__transport_enabled # Default configuration file: __default_rns_config__ = '''# This is the default Reticulum config file. @@ -467,7 +467,7 @@ loglevel = 4 # needs or turn it off completely. [[Default UDP Interface]] - type = UdpInterface + type = UdpInterface interface_enabled = True outgoing = True listen_ip = 0.0.0.0 @@ -564,7 +564,7 @@ loglevel = 4 # Allow transmit on interface. outgoing = true - # Serial port for the device + # Serial port for the device port = /dev/ttyUSB1 # Set the serial baud-rate and other @@ -621,7 +621,7 @@ loglevel = 4 # Allow transmit on interface. outgoing = true - # Serial port for the device + # Serial port for the device port = /dev/ttyUSB2 # Set the serial baud-rate and other diff --git a/RNS/Transport.py b/RNS/Transport.py index a1e5459..c34c251 100755 --- a/RNS/Transport.py +++ b/RNS/Transport.py @@ -9,1123 +9,1123 @@ from time import sleep from .vendor import umsgpack as umsgpack class Transport: - # Constants - BROADCAST = 0x00; - TRANSPORT = 0x01; - RELAY = 0x02; - TUNNEL = 0x03; - types = [BROADCAST, TRANSPORT, RELAY, TUNNEL] - - REACHABILITY_UNREACHABLE = 0x00 - REACHABILITY_DIRECT = 0x01 - REACHABILITY_TRANSPORT = 0x02 - - APP_NAME = "rnstransport" - - # TODO: Document the addition of random windows - # and max local rebroadcasts. - PATHFINDER_M = 18 # Max hops - PATHFINDER_C = 2.0 # Decay constant - PATHFINDER_R = 1 # Retransmit retries - PATHFINDER_T = 10 # Retry grace period - PATHFINDER_RW = 10 # Random window for announce rebroadcast - PATHFINDER_E = 60*15 # Path expiration in seconds - - # TODO: Calculate an optimal number for this in - # various situations - LOCAL_REBROADCASTS_MAX = 2 # How many local rebroadcasts of an announce is allowed - - PATH_REQUEST_GRACE = 0.35 # Grace time before a path announcement is made, allows directly reachable peers to respond first - PATH_REQUEST_RW = 2 # Path request random window - - LINK_TIMEOUT = RNS.Link.KEEPALIVE * 2 - REVERSE_TIMEOUT = 30*60 # Reverse table entries are removed after max 30 minutes - DESTINATION_TIMEOUT = 60*60*24*7 # Destination table entries are removed if unused for one week - MAX_RECEIPTS = 1024 # Maximum number of receipts to keep track of - - 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 - - # TODO: "destination_table" should really be renamed to "path_table" - announce_table = {} # A table for storing announces currently waiting to be retransmitted - destination_table = {} # A lookup table containing the next hop to a given destination - reverse_table = {} # A lookup table for storing packet hashes used to return proofs and replies - link_table = {} # A lookup table containing hops for links - held_announces = {} # A table containing temporarily held announce-table entries - - # Transport control destinations are used - # for control purposes like path requests - control_destinations = [] - control_hashes = [] - - # Interfaces for communicating with - # local clients connected to a shared - # Reticulum instance - local_client_interfaces = [] - - jobs_locked = False - jobs_running = False - job_interval = 0.250 - receipts_last_checked = 0.0 - receipts_check_interval = 1.0 - announces_last_checked = 0.0 - announces_check_interval = 1.0 - hashlist_maxsize = 1000000 - tables_last_culled = 0.0 - tables_cull_interval = 5.0 - - identity = None - - @staticmethod - def start(reticulum_instance): - Transport.owner = reticulum_instance - - if Transport.identity == None: - transport_identity_path = RNS.Reticulum.storagepath+"/transport_identity" - 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 in storage, creating...", RNS.LOG_VERBOSE) - Transport.identity = RNS.Identity() - Transport.identity.save(transport_identity_path) - else: - RNS.log("Loaded Transport Identity from storage", RNS.LOG_VERBOSE) - - packet_hashlist_path = RNS.Reticulum.storagepath+"/packet_hashlist" - if os.path.isfile(packet_hashlist_path): - try: - file = open(packet_hashlist_path, "rb") - Transport.packet_hashlist = umsgpack.unpackb(file.read()) - file.close() - except Exception as e: - RNS.log("Could not load packet hashlist from storage, the contained exception was: "+str(e), RNS.LOG_ERROR) - - # Create transport-specific destinations - Transport.path_request_destination = RNS.Destination(None, RNS.Destination.IN, RNS.Destination.PLAIN, Transport.APP_NAME, "path", "request") - Transport.path_request_destination.packet_callback(Transport.path_request_handler) - Transport.control_destinations.append(Transport.path_request_destination) - Transport.control_hashes.append(Transport.path_request_destination.hash) - - thread = threading.Thread(target=Transport.jobloop) - thread.setDaemon(True) - thread.start() - - if RNS.Reticulum.transport_enabled(): - destination_table_path = RNS.Reticulum.storagepath+"/destination_table" - if os.path.isfile(destination_table_path) and not Transport.owner.is_connected_to_shared_instance: - serialised_destinations = [] - try: - file = open(destination_table_path, "rb") - serialised_destinations = umsgpack.unpackb(file.read()) - file.close() - - for serialised_entry in serialised_destinations: - destination_hash = serialised_entry[0] - timestamp = serialised_entry[1] - received_from = serialised_entry[2] - hops = serialised_entry[3] - expires = serialised_entry[4] - random_blobs = serialised_entry[5] - receiving_interface = Transport.find_interface_from_hash(serialised_entry[6]) - announce_packet = Transport.get_cached_packet(serialised_entry[7]) - - if announce_packet != None and receiving_interface != None: - announce_packet.unpack() - # We increase the hops, since reading a packet - # from cache is equivalent to receiving it again - # over an interface. It is cached with it's non- - # increased hop-count. - announce_packet.hops += 1 - Transport.destination_table[destination_hash] = [timestamp, received_from, hops, expires, random_blobs, receiving_interface, announce_packet] - RNS.log("Loaded path table entry for "+RNS.prettyhexrep(destination_hash)+" from storage", RNS.LOG_DEBUG) - else: - RNS.log("Could not reconstruct path table entry from storage for "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG) - if announce_packet == None: - RNS.log("The announce packet could not be loaded from cache", RNS.LOG_DEBUG) - if receiving_interface == None: - RNS.log("The interface is no longer available", RNS.LOG_DEBUG) - - if len(Transport.destination_table) == 1: - specifier = "entry" - else: - specifier = "entries" - - RNS.log("Loaded "+str(len(Transport.destination_table))+" path table "+specifier+" from storage", RNS.LOG_VERBOSE) - - except Exception as e: - RNS.log("Could not load destination table from storage, the contained exception was: "+str(e), RNS.LOG_ERROR) - - RNS.log("Transport instance "+str(Transport.identity)+" started") - - @staticmethod - def jobloop(): - while (True): - Transport.jobs() - sleep(Transport.job_interval) - - @staticmethod - def jobs(): - outgoing = [] - Transport.jobs_running = True - try: - if not Transport.jobs_locked: - # Process receipts list for timed-out packets - if time.time() > Transport.receipts_last_checked+Transport.receipts_check_interval: - while len(Transport.receipts) > Transport.MAX_RECEIPTS: - culled_receipt = Transport.receipts.pop(0) - culled_receipt.timeout = -1 - receipt.check_timeout() - - for receipt in Transport.receipts: - receipt.check_timeout() - if receipt.status != RNS.PacketReceipt.SENT: - Transport.receipts.remove(receipt) - - Transport.receipts_last_checked = time.time() - - # 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] - if announce_entry[2] > Transport.PATHFINDER_R: - RNS.log("Dropping announce for "+RNS.prettyhexrep(destination_hash)+", retries exceeded", RNS.LOG_DEBUG) - Transport.announce_table.pop(destination_hash) - break - else: - if time.time() > announce_entry[1]: - announce_entry[1] = time.time() + math.pow(Transport.PATHFINDER_C, announce_entry[4]) + Transport.PATHFINDER_T + Transport.PATHFINDER_RW - announce_entry[2] += 1 - packet = announce_entry[5] - block_rebroadcasts = announce_entry[7] - attached_interface = announce_entry[8] - announce_context = RNS.Packet.NONE - if block_rebroadcasts: - announce_context = RNS.Packet.PATH_RESPONSE - 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.hex() - - new_packet = RNS.Packet( - announce_destination, - announce_data, - RNS.Packet.ANNOUNCE, - context = announce_context, - header_type = RNS.Packet.HEADER_2, - transport_type = Transport.TRANSPORT, - transport_id = Transport.identity.hash, - attached_interface = attached_interface - ) - - new_packet.hops = announce_entry[4] - if block_rebroadcasts: - RNS.log("Rebroadcasting announce as path response for "+RNS.prettyhexrep(announce_destination.hash)+" with hop count "+str(new_packet.hops), RNS.LOG_DEBUG) - else: - RNS.log("Rebroadcasting announce for "+RNS.prettyhexrep(announce_destination.hash)+" with hop count "+str(new_packet.hops), RNS.LOG_DEBUG) - outgoing.append(new_packet) - - # This handles an edge case where a peer sends a past - # request for a destination just after an announce for - # said destination has arrived, but before it has been - # rebroadcast locally. In such a case the actual announce - # is temporarily held, and then reinserted when the path - # request has been served to the peer. - if destination_hash in Transport.held_announces: - held_entry = Transport.held_announces.pop(destination_hash) - Transport.announce_table[destination_hash] = held_entry - RNS.log("Reinserting held announce into table", RNS.LOG_DEBUG) - - Transport.announces_last_checked = time.time() - - - # Cull the packet hashlist if it has reached max size - while (len(Transport.packet_hashlist) > Transport.hashlist_maxsize): - Transport.packet_hashlist.pop(0) - - if time.time() > Transport.tables_last_culled + Transport.tables_cull_interval: - # Cull the reverse table according to timeout - for truncated_packet_hash in Transport.reverse_table: - reverse_entry = Transport.reverse_table[truncated_packet_hash] - if time.time() > reverse_entry[2] + Transport.REVERSE_TIMEOUT: - Transport.reverse_table.pop(truncated_packet_hash) - - # Cull the link table according to timeout - stale_links = [] - for link_id in Transport.link_table: - link_entry = Transport.link_table[link_id] - if time.time() > link_entry[0] + Transport.LINK_TIMEOUT: - stale_links.append(link_id) - - # Cull the path table - stale_paths = [] - for destination_hash in Transport.destination_table: - destination_entry = Transport.destination_table[destination_hash] - attached_interface = destination_entry[5] - - if time.time() > destination_entry[0] + Transport.DESTINATION_TIMEOUT: - stale_paths.append(destination_hash) - RNS.log("Path to "+RNS.prettyhexrep(destination_hash)+" timed out and was removed", RNS.LOG_DEBUG) - - if not attached_interface in Transport.interfaces: - stale_paths.append(destination_hash) - RNS.log("Path to "+RNS.prettyhexrep(destination_hash)+" was removed since the attached interface no longer exists", RNS.LOG_DEBUG) - - i = 0 - for link_id in stale_links: - Transport.link_table.pop(link_id) - i += 1 - - if i > 0: - if i == 1: - RNS.log("Dropped "+str(i)+" link", RNS.LOG_DEBUG) - else: - RNS.log("Dropped "+str(i)+" links", RNS.LOG_DEBUG) - - i = 0 - for destination_hash in stale_paths: - Transport.destination_table.pop(destination_hash) - i += 1 - - if i > 0: - if i == 1: - RNS.log("Removed "+str(i)+" path", RNS.LOG_DEBUG) - else: - RNS.log("Removed "+str(i)+" paths", RNS.LOG_DEBUG) - - Transport.tables_last_culled = time.time() - - 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) - traceback.print_exc() - - Transport.jobs_running = False - - for packet in outgoing: - packet.send() - - @staticmethod - def outbound(packet): - while (Transport.jobs_running): - sleep(0.01) - - Transport.jobs_locked = True - # TODO: This updateHash call might be redundant - packet.updateHash() - sent = False - - # Check if we have a known path for the destination in the path table - if packet.packet_type != RNS.Packet.ANNOUNCE and packet.destination_hash in Transport.destination_table: - outbound_interface = Transport.destination_table[packet.destination_hash][5] - - # If there's more than one hop to the destination, and we know - # a path, we insert the packet into transport by adding the next - # transport nodes address to the header, and modifying the flags. - # This rule applies both for "normal" transport, and when connected - # to a local shared Reticulum instance. - if Transport.destination_table[packet.destination_hash][2] > 1: - if packet.header_type == RNS.Packet.HEADER_1: - # Insert packet into transport - new_flags = (RNS.Packet.HEADER_2) << 6 | (Transport.TRANSPORT) << 4 | (packet.flags & 0b00001111) - new_raw = struct.pack("!B", new_flags) - new_raw += packet.raw[1:2] - new_raw += Transport.destination_table[packet.destination_hash][1] - new_raw += packet.raw[2:] - # TODO: Remove at some point - # RNS.log("Packet was inserted into transport via "+RNS.prettyhexrep(Transport.destination_table[packet.destination_hash][1])+" on: "+str(outbound_interface), RNS.LOG_EXTREME) - outbound_interface.processOutgoing(new_raw) - Transport.destination_table[packet.destination_hash][0] = time.time() - sent = True - - # In the special case where we are connected to a local shared - # Reticulum instance, and the destination is one hop away, we - # also add transport headers to inject the packet into transport - # via the shared instance. Normally a packet for a destination - # one hop away would just be broadcast directly, but since we - # are "behind" a shared instance, we need to get that instance - # to transport it onto the network. - elif Transport.destination_table[packet.destination_hash][2] == 1 and Transport.owner.is_connected_to_shared_instance: - if packet.header_type == RNS.Packet.HEADER_1: - # Insert packet into transport - new_flags = (RNS.Packet.HEADER_2) << 6 | (Transport.TRANSPORT) << 4 | (packet.flags & 0b00001111) - new_raw = struct.pack("!B", new_flags) - new_raw += packet.raw[1:2] - new_raw += Transport.destination_table[packet.destination_hash][1] - new_raw += packet.raw[2:] - # TODO: Remove at some point - # RNS.log("Packet was inserted into transport via "+RNS.prettyhexrep(Transport.destination_table[packet.destination_hash][1])+" on: "+str(outbound_interface), RNS.LOG_EXTREME) - outbound_interface.processOutgoing(new_raw) - Transport.destination_table[packet.destination_hash][0] = time.time() - sent = True - - # If none of the above applies, we know the destination is - # directly reachable, and also on which interface, so we - # simply transmit the packet directly on that one. - else: - outbound_interface.processOutgoing(packet.raw) - sent = True - - # If we don't have a known path for the destination, we'll - # broadcast the packet on all outgoing interfaces, or the - # just the relevant interface if the packet has an attached - # interface, or belongs to a link. - else: - for interface in Transport.interfaces: - if interface.OUT: - should_transmit = True - if packet.destination.type == RNS.Destination.LINK: - if packet.destination.status == RNS.Link.CLOSED: - should_transmit = False - if interface != packet.destination.attached_interface: - should_transmit = False - if packet.attached_interface != None and interface != packet.attached_interface: - should_transmit = False - - if should_transmit: - RNS.log("Transmitting "+str(len(packet.raw))+" bytes on: "+str(interface), RNS.LOG_EXTREME) - RNS.log("Hash is "+RNS.prettyhexrep(packet.packet_hash), RNS.LOG_EXTREME) - interface.processOutgoing(packet.raw) - sent = True - - if sent: - packet.sent = True - packet.sent_at = time.time() - - # Don't generate receipt if it has been explicitly disabled - if (packet.create_receipt == True and - # Only generate receipts for DATA packets - packet.packet_type == RNS.Packet.DATA and - # Don't generate receipts for PLAIN destinations - packet.destination.type != RNS.Destination.PLAIN and - # Don't generate receipts for link-related packets - not (packet.context >= RNS.Packet.KEEPALIVE and packet.context <= RNS.Packet.LRPROOF) and - # Don't generate receipts for resource packets - not (packet.context >= RNS.Packet.RESOURCE and packet.context <= RNS.Packet.RESOURCE_RCL)): - - packet.receipt = RNS.PacketReceipt(packet) - Transport.receipts.append(packet.receipt) - - Transport.cache(packet) - - Transport.jobs_locked = False - return sent - - @staticmethod - def packet_filter(packet): - # TODO: Think long and hard about this. - # Is it even strictly necessary with the current - # transport rules? - if packet.context == RNS.Packet.KEEPALIVE: - return True - if packet.context == RNS.Packet.RESOURCE_REQ: - return True - if packet.context == RNS.Packet.RESOURCE_PRF: - return True - if packet.context == RNS.Packet.RESOURCE: - return True - if packet.context == RNS.Packet.CACHE_REQUEST: - return True - if packet.destination_type == RNS.Destination.PLAIN: - return True - - if not packet.packet_hash in Transport.packet_hashlist: - return True - else: - if packet.packet_type == RNS.Packet.ANNOUNCE: - return True - - RNS.log("Filtered packet with hash "+RNS.prettyhexrep(packet.packet_hash), RNS.LOG_DEBUG) - return False - - @staticmethod - def inbound(raw, interface=None): - while (Transport.jobs_running): - sleep(0.1) - - Transport.jobs_locked = True - - packet = RNS.Packet(None, raw) - packet.unpack() - packet.receiving_interface = interface - packet.hops += 1 - - RNS.log(str(interface)+" received packet with hash "+RNS.prettyhexrep(packet.packet_hash), RNS.LOG_EXTREME) - - if len(Transport.local_client_interfaces) > 0: - - if Transport.is_local_client_interface(interface): - packet.hops -= 1 - elif Transport.interface_to_shared_instance(interface): - packet.hops -= 1 - - - if Transport.packet_filter(packet): - Transport.packet_hashlist.append(packet.packet_hash) - Transport.cache(packet) - - # Check special conditions for local clients connected - # through a shared Reticulum instance - from_local_client = (packet.receiving_interface in Transport.local_client_interfaces) - for_local_client = (packet.packet_type != RNS.Packet.ANNOUNCE) and (packet.destination_hash in Transport.destination_table and Transport.destination_table[packet.destination_hash][2] == 0) - for_local_client_link = (packet.packet_type != RNS.Packet.ANNOUNCE) and (packet.destination_hash in Transport.link_table and Transport.link_table[packet.destination_hash][4] in Transport.local_client_interfaces) - for_local_client_link |= (packet.packet_type != RNS.Packet.ANNOUNCE) and (packet.destination_hash in Transport.link_table and Transport.link_table[packet.destination_hash][2] in Transport.local_client_interfaces) - proof_for_local_client = (packet.destination_hash in Transport.reverse_table) and (Transport.reverse_table[packet.destination_hash][0] in Transport.local_client_interfaces) - - # Plain broadcast packets from local clients are sent - # directly on all attached interfaces, since they are - # never injected into transport. - if not packet.destination_hash in Transport.control_hashes: - if packet.destination_type == RNS.Destination.PLAIN and packet.transport_type == Transport.BROADCAST: - # Send to all interfaces except the originator - if from_local_client: - for interface in Transport.interfaces: - if interface != packet.receiving_interface: - interface.processOutgoing(packet.raw) - # If the packet was not from a local client, send - # it directly to all local clients - else: - for interface in Transport.local_client_interfaces: - interface.processOutgoing(packet.raw) - - - # General transport handling. Takes care of directing - # packets according to transport tables and recording - # entries in reverse and link tables. - if RNS.Reticulum.transport_enabled() or from_local_client or for_local_client or for_local_client_link: - - # If there is no transport id, but the packet is - # for a local client, we generate the transport - # id (it was stripped on the previous hop, since - # we "spoof" the hop count for clients behind a - # shared instance, so they look directly reach- - # able), and reinsert, so the normal transport - # implementation can handle the packet. - if packet.transport_id == None and for_local_client: - packet.transport_id = Transport.identity.hash - - # If this is a cache request, and we can fullfill - # it, do so and stop processing. Otherwise resume - # normal processing. - if packet.context == RNS.Packet.CACHE_REQUEST: - if Transport.cache_request_packet(packet): - return - - # If the packet is in transport, check whether we - # are the designated next hop, and process it - # accordingly if we are. - if packet.transport_id != None and packet.packet_type != RNS.Packet.ANNOUNCE: - if packet.transport_id == Transport.identity.hash: - RNS.log("Received packet in transport for "+RNS.prettyhexrep(packet.destination_hash)+" with matching transport ID, transporting it...", RNS.LOG_DEBUG) - if packet.destination_hash in Transport.destination_table: - next_hop = Transport.destination_table[packet.destination_hash][1] - remaining_hops = Transport.destination_table[packet.destination_hash][2] - RNS.log("Next hop to destination is "+RNS.prettyhexrep(next_hop)+" with "+str(remaining_hops)+" hops remaining, transporting it.", RNS.LOG_DEBUG) - if remaining_hops > 1: - # Just increase hop count and transmit - new_raw = packet.raw[0:1] - new_raw += struct.pack("!B", packet.hops) - new_raw += next_hop - new_raw += packet.raw[12:] - elif remaining_hops == 1: - # Strip transport headers and transmit - new_flags = (RNS.Packet.HEADER_1) << 6 | (Transport.BROADCAST) << 4 | (packet.flags & 0b00001111) - new_raw = struct.pack("!B", new_flags) - new_raw += struct.pack("!B", packet.hops) - new_raw += packet.raw[12:] - elif remaining_hops == 0: - # Just increase hop count and transmit - new_raw = packet.raw[0:1] - new_raw += struct.pack("!B", packet.hops) - new_raw += packet.raw[2:] - - outbound_interface = Transport.destination_table[packet.destination_hash][5] - outbound_interface.processOutgoing(new_raw) - Transport.destination_table[packet.destination_hash][0] = time.time() - - if packet.packet_type == RNS.Packet.LINKREQUEST: - # Entry format is - link_entry = [ time.time(), # 0: Timestamp, - next_hop, # 1: Next-hop transport ID - outbound_interface, # 2: Next-hop interface - remaining_hops, # 3: Remaining hops - packet.receiving_interface, # 4: Received on interface - packet.hops, # 5: Taken hops - packet.destination_hash, # 6: Original destination hash - False] # 7: Validated - - Transport.link_table[packet.getTruncatedHash()] = link_entry - - else: - # Entry format is - reverse_entry = [ packet.receiving_interface, # 0: Received on interface - outbound_interface, # 1: Outbound interface - time.time()] # 2: Timestamp - - Transport.reverse_table[packet.getTruncatedHash()] = reverse_entry - - else: - # TODO: There should probably be some kind of REJECT - # mechanism here, to signal to the source that their - # expected path failed. - RNS.log("Got packet in transport, but no known path to final destination. Dropping packet.", RNS.LOG_DEBUG) - - # Link transport handling. Directs packets according - # to entries in the link tables - if packet.packet_type != RNS.Packet.ANNOUNCE and packet.packet_type != RNS.Packet.LINKREQUEST and packet.context != RNS.Packet.LRPROOF: - if packet.destination_hash in Transport.link_table: - link_entry = Transport.link_table[packet.destination_hash] - # If receiving and outbound interface is - # the same for this link, direction doesn't - # matter, and we simply send the packet on. - outbound_interface = None - if link_entry[2] == link_entry[4]: - # But check that taken hops matches one - # of the expectede values. - if packet.hops == link_entry[3] or packet.hops == link_entry[5]: - outbound_interface = link_entry[2] - else: - # If interfaces differ, we transmit on - # the opposite interface of what the - # packet was received on. - if packet.receiving_interface == link_entry[2]: - # Also check that expected hop count matches - if packet.hops == link_entry[3]: - outbound_interface = link_entry[4] - elif packet.receiving_interface == link_entry[4]: - # Also check that expected hop count matches - if packet.hops == link_entry[5]: - outbound_interface = link_entry[2] - - if outbound_interface != None: - new_raw = packet.raw[0:1] - new_raw += struct.pack("!B", packet.hops) - new_raw += packet.raw[2:] - outbound_interface.processOutgoing(new_raw) - Transport.link_table[packet.destination_hash][0] = time.time() - else: - pass - - - # Announce handling. Handles logic related to incoming - # announces, queueing rebroadcasts of these, and removal - # of queued announce rebroadcasts once handed to the next node. - if packet.packet_type == RNS.Packet.ANNOUNCE: - local_destination = next((d for d in Transport.destinations if d.hash == packet.destination_hash), None) - if local_destination == None and RNS.Identity.validateAnnounce(packet): - 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 - # announce in question from our pending table - if RNS.Reticulum.transport_enabled() and packet.destination_hash in Transport.announce_table: - announce_entry = Transport.announce_table[packet.destination_hash] - - if packet.hops-1 == announce_entry[4]: - RNS.log("Heard a local rebroadcast of announce for "+RNS.prettyhexrep(packet.destination_hash), RNS.LOG_DEBUG) - announce_entry[6] += 1 - if announce_entry[6] >= Transport.LOCAL_REBROADCASTS_MAX: - RNS.log("Max local rebroadcasts of announce for "+RNS.prettyhexrep(packet.destination_hash)+" reached, dropping announce from our table", RNS.LOG_DEBUG) - Transport.announce_table.pop(packet.destination_hash) - - if packet.hops-1 == announce_entry[4]+1 and announce_entry[2] > 0: - now = time.time() - if now < announce_entry[1]: - RNS.log("Rebroadcasted announce for "+RNS.prettyhexrep(packet.destination_hash)+" has been passed on to next node, no further tries needed", RNS.LOG_DEBUG) - Transport.announce_table.pop(packet.destination_hash) - - else: - received_from = packet.destination_hash - - # Check if this announce should be inserted into - # announce and destination tables - should_add = False - - # First, check that the announce is not for a destination - # local to this system, and that hops are less than the max - if (not any(packet.destination_hash == d.hash for d in Transport.destinations) and packet.hops < Transport.PATHFINDER_M+1): - random_blob = packet.data[RNS.Identity.DERKEYSIZE//8+10:RNS.Identity.DERKEYSIZE//8+20] - random_blobs = [] - if packet.destination_hash in Transport.destination_table: - random_blobs = Transport.destination_table[packet.destination_hash][4] - - # If we already have a path to the announced - # destination, but the hop count is equal or - # less, we'll update our tables. - if packet.hops <= Transport.destination_table[packet.destination_hash][2]: - # Make sure we haven't heard the random - # blob before, so announces can't be - # replayed to forge paths. - # TODO: Check whether this approach works - # under all circumstances - if not random_blob in random_blobs: - should_add = True - else: - should_add = False - 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]): - # We also check that the announce hash is - # different from ones we've already heard, - # to avoid loops in the network - if not random_blob in random_blobs: - # TODO: Check that this ^ approach actually - # works under all circumstances - 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: - 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 - announce_hops = packet.hops - local_rebroadcasts = 0 - block_rebroadcasts = False - attached_interface = None - retransmit_timeout = now + math.pow(Transport.PATHFINDER_C, packet.hops) + (RNS.rand() * Transport.PATHFINDER_RW) - - random_blobs.append(random_blob) - - if (RNS.Reticulum.transport_enabled() or Transport.from_local_client(packet)) and packet.context != RNS.Packet.PATH_RESPONSE: - # If the announce is from a local client, - # we announce it immediately, but only one - # time. - if Transport.from_local_client(packet): - retransmit_timeout = now - retries = Transport.PATHFINDER_R - - Transport.announce_table[packet.destination_hash] = [ - now, - retransmit_timeout, - retries, - received_from, - announce_hops, - packet, - local_rebroadcasts, - block_rebroadcasts, - attached_interface - ] - - # If we have any local clients connected, we re- - # transmit the announce to them immediately - if (len(Transport.local_client_interfaces)): - 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.hex() - announce_context = RNS.Packet.NONE - announce_data = packet.data - - new_announce = RNS.Packet( - announce_destination, - announce_data, - RNS.Packet.ANNOUNCE, - context = announce_context, - header_type = RNS.Packet.HEADER_2, - transport_type = Transport.TRANSPORT, - transport_id = Transport.identity.hash, - attached_interface = attached_interface - ) - - new_announce.hops = packet.hops - new_announce.send() - - Transport.destination_table[packet.destination_hash] = [now, received_from, announce_hops, expires, random_blobs, packet.receiving_interface, packet] - RNS.log("Path to "+RNS.prettyhexrep(packet.destination_hash)+" is now "+str(announce_hops)+" hops away via "+RNS.prettyhexrep(received_from)+" on "+str(packet.receiving_interface), RNS.LOG_VERBOSE) - - # Handling for linkrequests to local destinations - elif packet.packet_type == RNS.Packet.LINKREQUEST: - for destination in Transport.destinations: - if destination.hash == packet.destination_hash and destination.type == packet.destination_type: - packet.destination = destination - destination.receive(packet) - - # Handling for local data packets - elif packet.packet_type == RNS.Packet.DATA: - if packet.destination_type == RNS.Destination.LINK: - for link in Transport.active_links: - if link.link_id == packet.destination_hash: - packet.link = link - link.receive(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) - - if destination.proof_strategy == RNS.Destination.PROVE_ALL: - packet.prove() - - elif destination.proof_strategy == RNS.Destination.PROVE_APP: - if destination.callbacks.proof_requested: - if destination.callbacks.proof_requested(packet): - packet.prove() - - # Handling for proofs and link-request proofs - elif packet.packet_type == RNS.Packet.PROOF: - if packet.context == RNS.Packet.LRPROOF: - # This is a link request proof, check if it - # needs to be transported - if (RNS.Reticulum.transport_enabled() or for_local_client_link or from_local_client) and packet.destination_hash in Transport.link_table: - link_entry = Transport.link_table[packet.destination_hash] - if packet.receiving_interface == link_entry[2]: - # TODO: Should we validate the LR proof at each transport - # step before transporting it? - RNS.log("Link request proof received on correct interface, transporting it via "+str(link_entry[4]), RNS.LOG_DEBUG) - new_raw = packet.raw[0:1] - new_raw += struct.pack("!B", packet.hops) - new_raw += packet.raw[2:] - Transport.link_table[packet.destination_hash][7] = True - link_entry[4].processOutgoing(new_raw) - else: - RNS.log("Link request proof received on wrong interface, not transporting it.", RNS.LOG_DEBUG) - else: - # Check if we can deliver it to a local - # pending link - for link in Transport.pending_links: - if link.link_id == packet.destination_hash: - link.validateProof(packet) - - elif packet.context == RNS.Packet.RESOURCE_PRF: - for link in Transport.active_links: - if link.link_id == packet.destination_hash: - link.receive(packet) - else: - if packet.destination_type == RNS.Destination.LINK: - for link in Transport.active_links: - if link.link_id == packet.destination_hash: - packet.link = link - - if len(packet.data) == RNS.PacketReceipt.EXPL_LENGTH: - proof_hash = packet.data[:RNS.Identity.HASHLENGTH//8] - else: - proof_hash = None - - # Check if this proof neds to be transported - if (RNS.Reticulum.transport_enabled() or from_local_client or proof_for_local_client) and packet.destination_hash in Transport.reverse_table: - reverse_entry = Transport.reverse_table.pop(packet.destination_hash) - if packet.receiving_interface == reverse_entry[1]: - RNS.log("Proof received on correct interface, transporting it via "+str(reverse_entry[0]), RNS.LOG_DEBUG) - new_raw = packet.raw[0:1] - new_raw += struct.pack("!B", packet.hops) - new_raw += packet.raw[2:] - reverse_entry[0].processOutgoing(new_raw) - else: - RNS.log("Proof received on wrong interface, not transporting it.", RNS.LOG_DEBUG) - - 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 - - @staticmethod - def registerDestination(destination): - destination.MTU = RNS.Reticulum.MTU - if destination.direction == RNS.Destination.IN: - Transport.destinations.append(destination) - - @staticmethod - def deregister_destination(destination): - if destination in Transport.destinations: - Transport.destinations.remove(destination) - - @staticmethod - def registerLink(link): - RNS.log("Registering link "+str(link), RNS.LOG_DEBUG) - 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) - 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 find_interface_from_hash(interface_hash): - for interface in Transport.interfaces: - if interface.get_hash() == interface_hash: - return interface - - return None - - @staticmethod - def shouldCache(packet): - if packet.context == RNS.Packet.RESOURCE_PRF: - return True - - return False - - # When caching packets to storage, they are written - # exactly as they arrived over their interface. This - # means that they have not had their hop count - # increased yet! Take note of this when reading from - # the packet cache. - @staticmethod - def cache(packet, force_cache=False): - if RNS.Transport.shouldCache(packet) or force_cache: - try: - packet_hash = RNS.hexrep(packet.getHash(), delimit=False) - interface_reference = None - if packet.receiving_interface != None: - interface_reference = str(packet.receiving_interface) - - file = open(RNS.Reticulum.cachepath+"/"+packet_hash, "wb") - file.write(umsgpack.packb([packet.raw, interface_reference])) - file.close() - - except Exception as e: - RNS.log("Error writing packet to cache", RNS.LOG_ERROR) - RNS.log("The contained exception was: "+str(e)) - - @staticmethod - def get_cached_packet(packet_hash): - try: - packet_hash = RNS.hexrep(packet_hash, delimit=False) - path = RNS.Reticulum.cachepath+"/"+packet_hash - - if os.path.isfile(path): - file = open(path, "rb") - cached_data = umsgpack.unpackb(file.read()) - file.close() - - packet = RNS.Packet(None, cached_data[0]) - interface_reference = cached_data[1] - - for interface in Transport.interfaces: - if str(interface) == interface_reference: - packet.receiving_interface = interface - - return packet - else: - return None - except Exception as e: - RNS.log("Exception occurred while getting cached packet.", RNS.LOG_ERROR) - RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) - - @staticmethod - def cache_request_packet(packet): - if len(packet.data) == RNS.Identity.HASHLENGTH/8: - packet = Transport.get_cached_packet(packet.data) - - if packet != None: - # If the packet was retrieved from the local - # cache, replay it to the Transport instance, - # so that it can be directed towards it original - # destination. - Transport.inbound(packet.raw, packet.receiving_interface) - return True - else: - return False - else: - return False - - @staticmethod - def cache_request(packet_hash, destination): - cached_packet = Transport.get_cached_packet(packet_hash) - if cached_packet: - # The packet was found in the local cache, - # replay it to the Transport instance. - Transport.inbound(packet.raw, packet.receiving_interface) - else: - # The packet is not in the local cache, - # query the network. - RNS.Packet(destination, packet_hash, context = RNS.Packet.CACHE_REQUEST).send() - - @staticmethod - def hasPath(destination_hash): - if destination_hash in Transport.destination_table: - return True - else: - return False - - @staticmethod - def requestPath(destination_hash): - path_request_data = destination_hash + RNS.Identity.getRandomHash() - path_request_dst = RNS.Destination(None, RNS.Destination.OUT, RNS.Destination.PLAIN, Transport.APP_NAME, "path", "request") - packet = RNS.Packet(path_request_dst, path_request_data, packet_type = RNS.Packet.DATA, transport_type = RNS.Transport.BROADCAST, header_type = RNS.Packet.HEADER_1) - packet.send() - - @staticmethod - def requestPathOnInterface(destination_hash, interface): - path_request_data = destination_hash + RNS.Identity.getRandomHash() - path_request_dst = RNS.Destination(None, RNS.Destination.OUT, RNS.Destination.PLAIN, Transport.APP_NAME, "path", "request") - packet = RNS.Packet(path_request_dst, path_request_data, packet_type = RNS.Packet.DATA, transport_type = RNS.Transport.BROADCAST, header_type = RNS.Packet.HEADER_1, attached_interface = interface) - packet.send() - - @staticmethod - def path_request_handler(data, packet): - if len(data) >= RNS.Identity.TRUNCATED_HASHLENGTH//8: - Transport.pathRequest( - data[:RNS.Identity.TRUNCATED_HASHLENGTH//8], - Transport.from_local_client(packet), - packet.receiving_interface - ) - - @staticmethod - def pathRequest(destination_hash, is_from_local_client, attached_interface): - RNS.log("Path request for "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG) - - local_destination = next((d for d in Transport.destinations if d.hash == destination_hash), None) - if local_destination != None: - RNS.log("Destination is local to this system, announcing", RNS.LOG_DEBUG) - local_destination.announce(path_response=True) - - elif (RNS.Reticulum.transport_enabled() or is_from_local_client) and destination_hash in Transport.destination_table: - RNS.log("Path found, inserting announce for transmission", RNS.LOG_DEBUG) - packet = Transport.destination_table[destination_hash][6] - received_from = Transport.destination_table[destination_hash][5] - - now = time.time() - retries = Transport.PATHFINDER_R - local_rebroadcasts = 0 - block_rebroadcasts = True - announce_hops = packet.hops - - if is_from_local_client: - retransmit_timeout = now - else: - # TODO: Look at this timing - retransmit_timeout = now + Transport.PATH_REQUEST_GRACE # + (RNS.rand() * Transport.PATHFINDER_RW) - - # This handles an edge case where a peer sends a past - # request for a destination just after an announce for - # said destination has arrived, but before it has been - # rebroadcast locally. In such a case the actual announce - # is temporarily held, and then reinserted when the path - # request has been served to the peer. - if packet.destination_hash in Transport.announce_table: - held_entry = Transport.announce_table[packet.destination_hash] - Transport.held_announces[packet.destination_hash] = held_entry - - Transport.announce_table[packet.destination_hash] = [now, retransmit_timeout, retries, received_from, announce_hops, packet, local_rebroadcasts, block_rebroadcasts, attached_interface] - - elif is_from_local_client: - # Forward path request on all interfaces - # except the local client - for interface in Transport.interfaces: - if not interface == attached_interface: - Transport.requestPathOnInterface(destination_hash, interface) - - elif not is_from_local_client and len(Transport.local_client_interfaces) > 0: - # Forward the path request on all local - # client interfaces - for interface in Transport.local_client_interfaces: - Transport.requestPathOnInterface(destination_hash, interface) - - else: - RNS.log("No known path to requested destination, ignoring request", RNS.LOG_DEBUG) - - @staticmethod - def from_local_client(packet): - if hasattr(packet.receiving_interface, "parent_interface"): - return Transport.is_local_client_interface(packet.receiving_interface) - else: - return False - - @staticmethod - def is_local_client_interface(interface): - if hasattr(interface, "parent_interface"): - if hasattr(interface.parent_interface, "is_local_shared_instance"): - return True - else: - return False - else: - return False - - @staticmethod - def interface_to_shared_instance(interface): - if hasattr(interface, "is_connected_to_shared_instance"): - return True - else: - return False - - @staticmethod - def exitHandler(): - RNS.log("Saving packet hashlist to storage...", RNS.LOG_VERBOSE) - try: - packet_hashlist_path = RNS.Reticulum.storagepath+"/packet_hashlist" - file = open(packet_hashlist_path, "wb") - file.write(umsgpack.packb(Transport.packet_hashlist)) - file.close() - RNS.log("Done packet hashlist to storage", RNS.LOG_VERBOSE) - except Exception as e: - RNS.log("Could not save packet hashlist to storage, the contained exception was: "+str(e), RNS.LOG_ERROR) - - if not Transport.owner.is_connected_to_shared_instance: - RNS.log("Saving path table to storage...", RNS.LOG_VERBOSE) - try: - serialised_destinations = [] - for destination_hash in Transport.destination_table: - # Get the destination entry from the destination table - de = Transport.destination_table[destination_hash] - interface_hash = de[5].get_hash() - - # Only store destination table entry if the associated - # interface is still active - interface = Transport.find_interface_from_hash(interface_hash) - if interface != None: - # Get the destination entry from the destination table - de = Transport.destination_table[destination_hash] - timestamp = de[0] - received_from = de[1] - hops = de[2] - expires = de[3] - random_blobs = de[4] - packet_hash = de[6].getHash() - - serialised_entry = [ - destination_hash, - timestamp, - received_from, - hops, - expires, - random_blobs, - interface_hash, - packet_hash - ] - - serialised_destinations.append(serialised_entry) - - Transport.cache(de[6], force_cache=True) - - destination_table_path = RNS.Reticulum.storagepath+"/destination_table" - file = open(destination_table_path, "wb") - file.write(umsgpack.packb(serialised_destinations)) - file.close() - RNS.log("Done saving path table to storage", RNS.LOG_VERBOSE) - except Exception as e: - RNS.log("Could not save path table to storage, the contained exception was: "+str(e), RNS.LOG_ERROR) + # Constants + BROADCAST = 0x00; + TRANSPORT = 0x01; + RELAY = 0x02; + TUNNEL = 0x03; + types = [BROADCAST, TRANSPORT, RELAY, TUNNEL] + + REACHABILITY_UNREACHABLE = 0x00 + REACHABILITY_DIRECT = 0x01 + REACHABILITY_TRANSPORT = 0x02 + + APP_NAME = "rnstransport" + + # TODO: Document the addition of random windows + # and max local rebroadcasts. + PATHFINDER_M = 18 # Max hops + PATHFINDER_C = 2.0 # Decay constant + PATHFINDER_R = 1 # Retransmit retries + PATHFINDER_T = 10 # Retry grace period + PATHFINDER_RW = 10 # Random window for announce rebroadcast + PATHFINDER_E = 60*15 # Path expiration in seconds + + # TODO: Calculate an optimal number for this in + # various situations + LOCAL_REBROADCASTS_MAX = 2 # How many local rebroadcasts of an announce is allowed + + PATH_REQUEST_GRACE = 0.35 # Grace time before a path announcement is made, allows directly reachable peers to respond first + PATH_REQUEST_RW = 2 # Path request random window + + LINK_TIMEOUT = RNS.Link.KEEPALIVE * 2 + REVERSE_TIMEOUT = 30*60 # Reverse table entries are removed after max 30 minutes + DESTINATION_TIMEOUT = 60*60*24*7 # Destination table entries are removed if unused for one week + MAX_RECEIPTS = 1024 # Maximum number of receipts to keep track of + + 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 + + # TODO: "destination_table" should really be renamed to "path_table" + announce_table = {} # A table for storing announces currently waiting to be retransmitted + destination_table = {} # A lookup table containing the next hop to a given destination + reverse_table = {} # A lookup table for storing packet hashes used to return proofs and replies + link_table = {} # A lookup table containing hops for links + held_announces = {} # A table containing temporarily held announce-table entries + + # Transport control destinations are used + # for control purposes like path requests + control_destinations = [] + control_hashes = [] + + # Interfaces for communicating with + # local clients connected to a shared + # Reticulum instance + local_client_interfaces = [] + + jobs_locked = False + jobs_running = False + job_interval = 0.250 + receipts_last_checked = 0.0 + receipts_check_interval = 1.0 + announces_last_checked = 0.0 + announces_check_interval = 1.0 + hashlist_maxsize = 1000000 + tables_last_culled = 0.0 + tables_cull_interval = 5.0 + + identity = None + + @staticmethod + def start(reticulum_instance): + Transport.owner = reticulum_instance + + if Transport.identity == None: + transport_identity_path = RNS.Reticulum.storagepath+"/transport_identity" + 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 in storage, creating...", RNS.LOG_VERBOSE) + Transport.identity = RNS.Identity() + Transport.identity.save(transport_identity_path) + else: + RNS.log("Loaded Transport Identity from storage", RNS.LOG_VERBOSE) + + packet_hashlist_path = RNS.Reticulum.storagepath+"/packet_hashlist" + if os.path.isfile(packet_hashlist_path): + try: + file = open(packet_hashlist_path, "rb") + Transport.packet_hashlist = umsgpack.unpackb(file.read()) + file.close() + except Exception as e: + RNS.log("Could not load packet hashlist from storage, the contained exception was: "+str(e), RNS.LOG_ERROR) + + # Create transport-specific destinations + Transport.path_request_destination = RNS.Destination(None, RNS.Destination.IN, RNS.Destination.PLAIN, Transport.APP_NAME, "path", "request") + Transport.path_request_destination.packet_callback(Transport.path_request_handler) + Transport.control_destinations.append(Transport.path_request_destination) + Transport.control_hashes.append(Transport.path_request_destination.hash) + + thread = threading.Thread(target=Transport.jobloop) + thread.setDaemon(True) + thread.start() + + if RNS.Reticulum.transport_enabled(): + destination_table_path = RNS.Reticulum.storagepath+"/destination_table" + if os.path.isfile(destination_table_path) and not Transport.owner.is_connected_to_shared_instance: + serialised_destinations = [] + try: + file = open(destination_table_path, "rb") + serialised_destinations = umsgpack.unpackb(file.read()) + file.close() + + for serialised_entry in serialised_destinations: + destination_hash = serialised_entry[0] + timestamp = serialised_entry[1] + received_from = serialised_entry[2] + hops = serialised_entry[3] + expires = serialised_entry[4] + random_blobs = serialised_entry[5] + receiving_interface = Transport.find_interface_from_hash(serialised_entry[6]) + announce_packet = Transport.get_cached_packet(serialised_entry[7]) + + if announce_packet != None and receiving_interface != None: + announce_packet.unpack() + # We increase the hops, since reading a packet + # from cache is equivalent to receiving it again + # over an interface. It is cached with it's non- + # increased hop-count. + announce_packet.hops += 1 + Transport.destination_table[destination_hash] = [timestamp, received_from, hops, expires, random_blobs, receiving_interface, announce_packet] + RNS.log("Loaded path table entry for "+RNS.prettyhexrep(destination_hash)+" from storage", RNS.LOG_DEBUG) + else: + RNS.log("Could not reconstruct path table entry from storage for "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG) + if announce_packet == None: + RNS.log("The announce packet could not be loaded from cache", RNS.LOG_DEBUG) + if receiving_interface == None: + RNS.log("The interface is no longer available", RNS.LOG_DEBUG) + + if len(Transport.destination_table) == 1: + specifier = "entry" + else: + specifier = "entries" + + RNS.log("Loaded "+str(len(Transport.destination_table))+" path table "+specifier+" from storage", RNS.LOG_VERBOSE) + + except Exception as e: + RNS.log("Could not load destination table from storage, the contained exception was: "+str(e), RNS.LOG_ERROR) + + RNS.log("Transport instance "+str(Transport.identity)+" started") + + @staticmethod + def jobloop(): + while (True): + Transport.jobs() + sleep(Transport.job_interval) + + @staticmethod + def jobs(): + outgoing = [] + Transport.jobs_running = True + try: + if not Transport.jobs_locked: + # Process receipts list for timed-out packets + if time.time() > Transport.receipts_last_checked+Transport.receipts_check_interval: + while len(Transport.receipts) > Transport.MAX_RECEIPTS: + culled_receipt = Transport.receipts.pop(0) + culled_receipt.timeout = -1 + receipt.check_timeout() + + for receipt in Transport.receipts: + receipt.check_timeout() + if receipt.status != RNS.PacketReceipt.SENT: + Transport.receipts.remove(receipt) + + Transport.receipts_last_checked = time.time() + + # 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] + if announce_entry[2] > Transport.PATHFINDER_R: + RNS.log("Dropping announce for "+RNS.prettyhexrep(destination_hash)+", retries exceeded", RNS.LOG_DEBUG) + Transport.announce_table.pop(destination_hash) + break + else: + if time.time() > announce_entry[1]: + announce_entry[1] = time.time() + math.pow(Transport.PATHFINDER_C, announce_entry[4]) + Transport.PATHFINDER_T + Transport.PATHFINDER_RW + announce_entry[2] += 1 + packet = announce_entry[5] + block_rebroadcasts = announce_entry[7] + attached_interface = announce_entry[8] + announce_context = RNS.Packet.NONE + if block_rebroadcasts: + announce_context = RNS.Packet.PATH_RESPONSE + 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.hex() + + new_packet = RNS.Packet( + announce_destination, + announce_data, + RNS.Packet.ANNOUNCE, + context = announce_context, + header_type = RNS.Packet.HEADER_2, + transport_type = Transport.TRANSPORT, + transport_id = Transport.identity.hash, + attached_interface = attached_interface + ) + + new_packet.hops = announce_entry[4] + if block_rebroadcasts: + RNS.log("Rebroadcasting announce as path response for "+RNS.prettyhexrep(announce_destination.hash)+" with hop count "+str(new_packet.hops), RNS.LOG_DEBUG) + else: + RNS.log("Rebroadcasting announce for "+RNS.prettyhexrep(announce_destination.hash)+" with hop count "+str(new_packet.hops), RNS.LOG_DEBUG) + outgoing.append(new_packet) + + # This handles an edge case where a peer sends a past + # request for a destination just after an announce for + # said destination has arrived, but before it has been + # rebroadcast locally. In such a case the actual announce + # is temporarily held, and then reinserted when the path + # request has been served to the peer. + if destination_hash in Transport.held_announces: + held_entry = Transport.held_announces.pop(destination_hash) + Transport.announce_table[destination_hash] = held_entry + RNS.log("Reinserting held announce into table", RNS.LOG_DEBUG) + + Transport.announces_last_checked = time.time() + + + # Cull the packet hashlist if it has reached max size + while (len(Transport.packet_hashlist) > Transport.hashlist_maxsize): + Transport.packet_hashlist.pop(0) + + if time.time() > Transport.tables_last_culled + Transport.tables_cull_interval: + # Cull the reverse table according to timeout + for truncated_packet_hash in Transport.reverse_table: + reverse_entry = Transport.reverse_table[truncated_packet_hash] + if time.time() > reverse_entry[2] + Transport.REVERSE_TIMEOUT: + Transport.reverse_table.pop(truncated_packet_hash) + + # Cull the link table according to timeout + stale_links = [] + for link_id in Transport.link_table: + link_entry = Transport.link_table[link_id] + if time.time() > link_entry[0] + Transport.LINK_TIMEOUT: + stale_links.append(link_id) + + # Cull the path table + stale_paths = [] + for destination_hash in Transport.destination_table: + destination_entry = Transport.destination_table[destination_hash] + attached_interface = destination_entry[5] + + if time.time() > destination_entry[0] + Transport.DESTINATION_TIMEOUT: + stale_paths.append(destination_hash) + RNS.log("Path to "+RNS.prettyhexrep(destination_hash)+" timed out and was removed", RNS.LOG_DEBUG) + + if not attached_interface in Transport.interfaces: + stale_paths.append(destination_hash) + RNS.log("Path to "+RNS.prettyhexrep(destination_hash)+" was removed since the attached interface no longer exists", RNS.LOG_DEBUG) + + i = 0 + for link_id in stale_links: + Transport.link_table.pop(link_id) + i += 1 + + if i > 0: + if i == 1: + RNS.log("Dropped "+str(i)+" link", RNS.LOG_DEBUG) + else: + RNS.log("Dropped "+str(i)+" links", RNS.LOG_DEBUG) + + i = 0 + for destination_hash in stale_paths: + Transport.destination_table.pop(destination_hash) + i += 1 + + if i > 0: + if i == 1: + RNS.log("Removed "+str(i)+" path", RNS.LOG_DEBUG) + else: + RNS.log("Removed "+str(i)+" paths", RNS.LOG_DEBUG) + + Transport.tables_last_culled = time.time() + + 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) + traceback.print_exc() + + Transport.jobs_running = False + + for packet in outgoing: + packet.send() + + @staticmethod + def outbound(packet): + while (Transport.jobs_running): + sleep(0.01) + + Transport.jobs_locked = True + # TODO: This updateHash call might be redundant + packet.updateHash() + sent = False + + # Check if we have a known path for the destination in the path table + if packet.packet_type != RNS.Packet.ANNOUNCE and packet.destination_hash in Transport.destination_table: + outbound_interface = Transport.destination_table[packet.destination_hash][5] + + # If there's more than one hop to the destination, and we know + # a path, we insert the packet into transport by adding the next + # transport nodes address to the header, and modifying the flags. + # This rule applies both for "normal" transport, and when connected + # to a local shared Reticulum instance. + if Transport.destination_table[packet.destination_hash][2] > 1: + if packet.header_type == RNS.Packet.HEADER_1: + # Insert packet into transport + new_flags = (RNS.Packet.HEADER_2) << 6 | (Transport.TRANSPORT) << 4 | (packet.flags & 0b00001111) + new_raw = struct.pack("!B", new_flags) + new_raw += packet.raw[1:2] + new_raw += Transport.destination_table[packet.destination_hash][1] + new_raw += packet.raw[2:] + # TODO: Remove at some point + # RNS.log("Packet was inserted into transport via "+RNS.prettyhexrep(Transport.destination_table[packet.destination_hash][1])+" on: "+str(outbound_interface), RNS.LOG_EXTREME) + outbound_interface.processOutgoing(new_raw) + Transport.destination_table[packet.destination_hash][0] = time.time() + sent = True + + # In the special case where we are connected to a local shared + # Reticulum instance, and the destination is one hop away, we + # also add transport headers to inject the packet into transport + # via the shared instance. Normally a packet for a destination + # one hop away would just be broadcast directly, but since we + # are "behind" a shared instance, we need to get that instance + # to transport it onto the network. + elif Transport.destination_table[packet.destination_hash][2] == 1 and Transport.owner.is_connected_to_shared_instance: + if packet.header_type == RNS.Packet.HEADER_1: + # Insert packet into transport + new_flags = (RNS.Packet.HEADER_2) << 6 | (Transport.TRANSPORT) << 4 | (packet.flags & 0b00001111) + new_raw = struct.pack("!B", new_flags) + new_raw += packet.raw[1:2] + new_raw += Transport.destination_table[packet.destination_hash][1] + new_raw += packet.raw[2:] + # TODO: Remove at some point + # RNS.log("Packet was inserted into transport via "+RNS.prettyhexrep(Transport.destination_table[packet.destination_hash][1])+" on: "+str(outbound_interface), RNS.LOG_EXTREME) + outbound_interface.processOutgoing(new_raw) + Transport.destination_table[packet.destination_hash][0] = time.time() + sent = True + + # If none of the above applies, we know the destination is + # directly reachable, and also on which interface, so we + # simply transmit the packet directly on that one. + else: + outbound_interface.processOutgoing(packet.raw) + sent = True + + # If we don't have a known path for the destination, we'll + # broadcast the packet on all outgoing interfaces, or the + # just the relevant interface if the packet has an attached + # interface, or belongs to a link. + else: + for interface in Transport.interfaces: + if interface.OUT: + should_transmit = True + if packet.destination.type == RNS.Destination.LINK: + if packet.destination.status == RNS.Link.CLOSED: + should_transmit = False + if interface != packet.destination.attached_interface: + should_transmit = False + if packet.attached_interface != None and interface != packet.attached_interface: + should_transmit = False + + if should_transmit: + RNS.log("Transmitting "+str(len(packet.raw))+" bytes on: "+str(interface), RNS.LOG_EXTREME) + RNS.log("Hash is "+RNS.prettyhexrep(packet.packet_hash), RNS.LOG_EXTREME) + interface.processOutgoing(packet.raw) + sent = True + + if sent: + packet.sent = True + packet.sent_at = time.time() + + # Don't generate receipt if it has been explicitly disabled + if (packet.create_receipt == True and + # Only generate receipts for DATA packets + packet.packet_type == RNS.Packet.DATA and + # Don't generate receipts for PLAIN destinations + packet.destination.type != RNS.Destination.PLAIN and + # Don't generate receipts for link-related packets + not (packet.context >= RNS.Packet.KEEPALIVE and packet.context <= RNS.Packet.LRPROOF) and + # Don't generate receipts for resource packets + not (packet.context >= RNS.Packet.RESOURCE and packet.context <= RNS.Packet.RESOURCE_RCL)): + + packet.receipt = RNS.PacketReceipt(packet) + Transport.receipts.append(packet.receipt) + + Transport.cache(packet) + + Transport.jobs_locked = False + return sent + + @staticmethod + def packet_filter(packet): + # TODO: Think long and hard about this. + # Is it even strictly necessary with the current + # transport rules? + if packet.context == RNS.Packet.KEEPALIVE: + return True + if packet.context == RNS.Packet.RESOURCE_REQ: + return True + if packet.context == RNS.Packet.RESOURCE_PRF: + return True + if packet.context == RNS.Packet.RESOURCE: + return True + if packet.context == RNS.Packet.CACHE_REQUEST: + return True + if packet.destination_type == RNS.Destination.PLAIN: + return True + + if not packet.packet_hash in Transport.packet_hashlist: + return True + else: + if packet.packet_type == RNS.Packet.ANNOUNCE: + return True + + RNS.log("Filtered packet with hash "+RNS.prettyhexrep(packet.packet_hash), RNS.LOG_DEBUG) + return False + + @staticmethod + def inbound(raw, interface=None): + while (Transport.jobs_running): + sleep(0.1) + + Transport.jobs_locked = True + + packet = RNS.Packet(None, raw) + packet.unpack() + packet.receiving_interface = interface + packet.hops += 1 + + RNS.log(str(interface)+" received packet with hash "+RNS.prettyhexrep(packet.packet_hash), RNS.LOG_EXTREME) + + if len(Transport.local_client_interfaces) > 0: + + if Transport.is_local_client_interface(interface): + packet.hops -= 1 + elif Transport.interface_to_shared_instance(interface): + packet.hops -= 1 + + + if Transport.packet_filter(packet): + Transport.packet_hashlist.append(packet.packet_hash) + Transport.cache(packet) + + # Check special conditions for local clients connected + # through a shared Reticulum instance + from_local_client = (packet.receiving_interface in Transport.local_client_interfaces) + for_local_client = (packet.packet_type != RNS.Packet.ANNOUNCE) and (packet.destination_hash in Transport.destination_table and Transport.destination_table[packet.destination_hash][2] == 0) + for_local_client_link = (packet.packet_type != RNS.Packet.ANNOUNCE) and (packet.destination_hash in Transport.link_table and Transport.link_table[packet.destination_hash][4] in Transport.local_client_interfaces) + for_local_client_link |= (packet.packet_type != RNS.Packet.ANNOUNCE) and (packet.destination_hash in Transport.link_table and Transport.link_table[packet.destination_hash][2] in Transport.local_client_interfaces) + proof_for_local_client = (packet.destination_hash in Transport.reverse_table) and (Transport.reverse_table[packet.destination_hash][0] in Transport.local_client_interfaces) + + # Plain broadcast packets from local clients are sent + # directly on all attached interfaces, since they are + # never injected into transport. + if not packet.destination_hash in Transport.control_hashes: + if packet.destination_type == RNS.Destination.PLAIN and packet.transport_type == Transport.BROADCAST: + # Send to all interfaces except the originator + if from_local_client: + for interface in Transport.interfaces: + if interface != packet.receiving_interface: + interface.processOutgoing(packet.raw) + # If the packet was not from a local client, send + # it directly to all local clients + else: + for interface in Transport.local_client_interfaces: + interface.processOutgoing(packet.raw) + + + # General transport handling. Takes care of directing + # packets according to transport tables and recording + # entries in reverse and link tables. + if RNS.Reticulum.transport_enabled() or from_local_client or for_local_client or for_local_client_link: + + # If there is no transport id, but the packet is + # for a local client, we generate the transport + # id (it was stripped on the previous hop, since + # we "spoof" the hop count for clients behind a + # shared instance, so they look directly reach- + # able), and reinsert, so the normal transport + # implementation can handle the packet. + if packet.transport_id == None and for_local_client: + packet.transport_id = Transport.identity.hash + + # If this is a cache request, and we can fullfill + # it, do so and stop processing. Otherwise resume + # normal processing. + if packet.context == RNS.Packet.CACHE_REQUEST: + if Transport.cache_request_packet(packet): + return + + # If the packet is in transport, check whether we + # are the designated next hop, and process it + # accordingly if we are. + if packet.transport_id != None and packet.packet_type != RNS.Packet.ANNOUNCE: + if packet.transport_id == Transport.identity.hash: + RNS.log("Received packet in transport for "+RNS.prettyhexrep(packet.destination_hash)+" with matching transport ID, transporting it...", RNS.LOG_DEBUG) + if packet.destination_hash in Transport.destination_table: + next_hop = Transport.destination_table[packet.destination_hash][1] + remaining_hops = Transport.destination_table[packet.destination_hash][2] + RNS.log("Next hop to destination is "+RNS.prettyhexrep(next_hop)+" with "+str(remaining_hops)+" hops remaining, transporting it.", RNS.LOG_DEBUG) + if remaining_hops > 1: + # Just increase hop count and transmit + new_raw = packet.raw[0:1] + new_raw += struct.pack("!B", packet.hops) + new_raw += next_hop + new_raw += packet.raw[12:] + elif remaining_hops == 1: + # Strip transport headers and transmit + new_flags = (RNS.Packet.HEADER_1) << 6 | (Transport.BROADCAST) << 4 | (packet.flags & 0b00001111) + new_raw = struct.pack("!B", new_flags) + new_raw += struct.pack("!B", packet.hops) + new_raw += packet.raw[12:] + elif remaining_hops == 0: + # Just increase hop count and transmit + new_raw = packet.raw[0:1] + new_raw += struct.pack("!B", packet.hops) + new_raw += packet.raw[2:] + + outbound_interface = Transport.destination_table[packet.destination_hash][5] + outbound_interface.processOutgoing(new_raw) + Transport.destination_table[packet.destination_hash][0] = time.time() + + if packet.packet_type == RNS.Packet.LINKREQUEST: + # Entry format is + link_entry = [ time.time(), # 0: Timestamp, + next_hop, # 1: Next-hop transport ID + outbound_interface, # 2: Next-hop interface + remaining_hops, # 3: Remaining hops + packet.receiving_interface, # 4: Received on interface + packet.hops, # 5: Taken hops + packet.destination_hash, # 6: Original destination hash + False] # 7: Validated + + Transport.link_table[packet.getTruncatedHash()] = link_entry + + else: + # Entry format is + reverse_entry = [ packet.receiving_interface, # 0: Received on interface + outbound_interface, # 1: Outbound interface + time.time()] # 2: Timestamp + + Transport.reverse_table[packet.getTruncatedHash()] = reverse_entry + + else: + # TODO: There should probably be some kind of REJECT + # mechanism here, to signal to the source that their + # expected path failed. + RNS.log("Got packet in transport, but no known path to final destination. Dropping packet.", RNS.LOG_DEBUG) + + # Link transport handling. Directs packets according + # to entries in the link tables + if packet.packet_type != RNS.Packet.ANNOUNCE and packet.packet_type != RNS.Packet.LINKREQUEST and packet.context != RNS.Packet.LRPROOF: + if packet.destination_hash in Transport.link_table: + link_entry = Transport.link_table[packet.destination_hash] + # If receiving and outbound interface is + # the same for this link, direction doesn't + # matter, and we simply send the packet on. + outbound_interface = None + if link_entry[2] == link_entry[4]: + # But check that taken hops matches one + # of the expectede values. + if packet.hops == link_entry[3] or packet.hops == link_entry[5]: + outbound_interface = link_entry[2] + else: + # If interfaces differ, we transmit on + # the opposite interface of what the + # packet was received on. + if packet.receiving_interface == link_entry[2]: + # Also check that expected hop count matches + if packet.hops == link_entry[3]: + outbound_interface = link_entry[4] + elif packet.receiving_interface == link_entry[4]: + # Also check that expected hop count matches + if packet.hops == link_entry[5]: + outbound_interface = link_entry[2] + + if outbound_interface != None: + new_raw = packet.raw[0:1] + new_raw += struct.pack("!B", packet.hops) + new_raw += packet.raw[2:] + outbound_interface.processOutgoing(new_raw) + Transport.link_table[packet.destination_hash][0] = time.time() + else: + pass + + + # Announce handling. Handles logic related to incoming + # announces, queueing rebroadcasts of these, and removal + # of queued announce rebroadcasts once handed to the next node. + if packet.packet_type == RNS.Packet.ANNOUNCE: + local_destination = next((d for d in Transport.destinations if d.hash == packet.destination_hash), None) + if local_destination == None and RNS.Identity.validateAnnounce(packet): + 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 + # announce in question from our pending table + if RNS.Reticulum.transport_enabled() and packet.destination_hash in Transport.announce_table: + announce_entry = Transport.announce_table[packet.destination_hash] + + if packet.hops-1 == announce_entry[4]: + RNS.log("Heard a local rebroadcast of announce for "+RNS.prettyhexrep(packet.destination_hash), RNS.LOG_DEBUG) + announce_entry[6] += 1 + if announce_entry[6] >= Transport.LOCAL_REBROADCASTS_MAX: + RNS.log("Max local rebroadcasts of announce for "+RNS.prettyhexrep(packet.destination_hash)+" reached, dropping announce from our table", RNS.LOG_DEBUG) + Transport.announce_table.pop(packet.destination_hash) + + if packet.hops-1 == announce_entry[4]+1 and announce_entry[2] > 0: + now = time.time() + if now < announce_entry[1]: + RNS.log("Rebroadcasted announce for "+RNS.prettyhexrep(packet.destination_hash)+" has been passed on to next node, no further tries needed", RNS.LOG_DEBUG) + Transport.announce_table.pop(packet.destination_hash) + + else: + received_from = packet.destination_hash + + # Check if this announce should be inserted into + # announce and destination tables + should_add = False + + # First, check that the announce is not for a destination + # local to this system, and that hops are less than the max + if (not any(packet.destination_hash == d.hash for d in Transport.destinations) and packet.hops < Transport.PATHFINDER_M+1): + random_blob = packet.data[RNS.Identity.DERKEYSIZE//8+10:RNS.Identity.DERKEYSIZE//8+20] + random_blobs = [] + if packet.destination_hash in Transport.destination_table: + random_blobs = Transport.destination_table[packet.destination_hash][4] + + # If we already have a path to the announced + # destination, but the hop count is equal or + # less, we'll update our tables. + if packet.hops <= Transport.destination_table[packet.destination_hash][2]: + # Make sure we haven't heard the random + # blob before, so announces can't be + # replayed to forge paths. + # TODO: Check whether this approach works + # under all circumstances + if not random_blob in random_blobs: + should_add = True + else: + should_add = False + 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]): + # We also check that the announce hash is + # different from ones we've already heard, + # to avoid loops in the network + if not random_blob in random_blobs: + # TODO: Check that this ^ approach actually + # works under all circumstances + 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: + 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 + announce_hops = packet.hops + local_rebroadcasts = 0 + block_rebroadcasts = False + attached_interface = None + retransmit_timeout = now + math.pow(Transport.PATHFINDER_C, packet.hops) + (RNS.rand() * Transport.PATHFINDER_RW) + + random_blobs.append(random_blob) + + if (RNS.Reticulum.transport_enabled() or Transport.from_local_client(packet)) and packet.context != RNS.Packet.PATH_RESPONSE: + # If the announce is from a local client, + # we announce it immediately, but only one + # time. + if Transport.from_local_client(packet): + retransmit_timeout = now + retries = Transport.PATHFINDER_R + + Transport.announce_table[packet.destination_hash] = [ + now, + retransmit_timeout, + retries, + received_from, + announce_hops, + packet, + local_rebroadcasts, + block_rebroadcasts, + attached_interface + ] + + # If we have any local clients connected, we re- + # transmit the announce to them immediately + if (len(Transport.local_client_interfaces)): + 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.hex() + announce_context = RNS.Packet.NONE + announce_data = packet.data + + new_announce = RNS.Packet( + announce_destination, + announce_data, + RNS.Packet.ANNOUNCE, + context = announce_context, + header_type = RNS.Packet.HEADER_2, + transport_type = Transport.TRANSPORT, + transport_id = Transport.identity.hash, + attached_interface = attached_interface + ) + + new_announce.hops = packet.hops + new_announce.send() + + Transport.destination_table[packet.destination_hash] = [now, received_from, announce_hops, expires, random_blobs, packet.receiving_interface, packet] + RNS.log("Path to "+RNS.prettyhexrep(packet.destination_hash)+" is now "+str(announce_hops)+" hops away via "+RNS.prettyhexrep(received_from)+" on "+str(packet.receiving_interface), RNS.LOG_VERBOSE) + + # Handling for linkrequests to local destinations + elif packet.packet_type == RNS.Packet.LINKREQUEST: + for destination in Transport.destinations: + if destination.hash == packet.destination_hash and destination.type == packet.destination_type: + packet.destination = destination + destination.receive(packet) + + # Handling for local data packets + elif packet.packet_type == RNS.Packet.DATA: + if packet.destination_type == RNS.Destination.LINK: + for link in Transport.active_links: + if link.link_id == packet.destination_hash: + packet.link = link + link.receive(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) + + if destination.proof_strategy == RNS.Destination.PROVE_ALL: + packet.prove() + + elif destination.proof_strategy == RNS.Destination.PROVE_APP: + if destination.callbacks.proof_requested: + if destination.callbacks.proof_requested(packet): + packet.prove() + + # Handling for proofs and link-request proofs + elif packet.packet_type == RNS.Packet.PROOF: + if packet.context == RNS.Packet.LRPROOF: + # This is a link request proof, check if it + # needs to be transported + if (RNS.Reticulum.transport_enabled() or for_local_client_link or from_local_client) and packet.destination_hash in Transport.link_table: + link_entry = Transport.link_table[packet.destination_hash] + if packet.receiving_interface == link_entry[2]: + # TODO: Should we validate the LR proof at each transport + # step before transporting it? + RNS.log("Link request proof received on correct interface, transporting it via "+str(link_entry[4]), RNS.LOG_DEBUG) + new_raw = packet.raw[0:1] + new_raw += struct.pack("!B", packet.hops) + new_raw += packet.raw[2:] + Transport.link_table[packet.destination_hash][7] = True + link_entry[4].processOutgoing(new_raw) + else: + RNS.log("Link request proof received on wrong interface, not transporting it.", RNS.LOG_DEBUG) + else: + # Check if we can deliver it to a local + # pending link + for link in Transport.pending_links: + if link.link_id == packet.destination_hash: + link.validateProof(packet) + + elif packet.context == RNS.Packet.RESOURCE_PRF: + for link in Transport.active_links: + if link.link_id == packet.destination_hash: + link.receive(packet) + else: + if packet.destination_type == RNS.Destination.LINK: + for link in Transport.active_links: + if link.link_id == packet.destination_hash: + packet.link = link + + if len(packet.data) == RNS.PacketReceipt.EXPL_LENGTH: + proof_hash = packet.data[:RNS.Identity.HASHLENGTH//8] + else: + proof_hash = None + + # Check if this proof neds to be transported + if (RNS.Reticulum.transport_enabled() or from_local_client or proof_for_local_client) and packet.destination_hash in Transport.reverse_table: + reverse_entry = Transport.reverse_table.pop(packet.destination_hash) + if packet.receiving_interface == reverse_entry[1]: + RNS.log("Proof received on correct interface, transporting it via "+str(reverse_entry[0]), RNS.LOG_DEBUG) + new_raw = packet.raw[0:1] + new_raw += struct.pack("!B", packet.hops) + new_raw += packet.raw[2:] + reverse_entry[0].processOutgoing(new_raw) + else: + RNS.log("Proof received on wrong interface, not transporting it.", RNS.LOG_DEBUG) + + 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 + + @staticmethod + def registerDestination(destination): + destination.MTU = RNS.Reticulum.MTU + if destination.direction == RNS.Destination.IN: + Transport.destinations.append(destination) + + @staticmethod + def deregister_destination(destination): + if destination in Transport.destinations: + Transport.destinations.remove(destination) + + @staticmethod + def registerLink(link): + RNS.log("Registering link "+str(link), RNS.LOG_DEBUG) + 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) + 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 find_interface_from_hash(interface_hash): + for interface in Transport.interfaces: + if interface.get_hash() == interface_hash: + return interface + + return None + + @staticmethod + def shouldCache(packet): + if packet.context == RNS.Packet.RESOURCE_PRF: + return True + + return False + + # When caching packets to storage, they are written + # exactly as they arrived over their interface. This + # means that they have not had their hop count + # increased yet! Take note of this when reading from + # the packet cache. + @staticmethod + def cache(packet, force_cache=False): + if RNS.Transport.shouldCache(packet) or force_cache: + try: + packet_hash = RNS.hexrep(packet.getHash(), delimit=False) + interface_reference = None + if packet.receiving_interface != None: + interface_reference = str(packet.receiving_interface) + + file = open(RNS.Reticulum.cachepath+"/"+packet_hash, "wb") + file.write(umsgpack.packb([packet.raw, interface_reference])) + file.close() + + except Exception as e: + RNS.log("Error writing packet to cache", RNS.LOG_ERROR) + RNS.log("The contained exception was: "+str(e)) + + @staticmethod + def get_cached_packet(packet_hash): + try: + packet_hash = RNS.hexrep(packet_hash, delimit=False) + path = RNS.Reticulum.cachepath+"/"+packet_hash + + if os.path.isfile(path): + file = open(path, "rb") + cached_data = umsgpack.unpackb(file.read()) + file.close() + + packet = RNS.Packet(None, cached_data[0]) + interface_reference = cached_data[1] + + for interface in Transport.interfaces: + if str(interface) == interface_reference: + packet.receiving_interface = interface + + return packet + else: + return None + except Exception as e: + RNS.log("Exception occurred while getting cached packet.", RNS.LOG_ERROR) + RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) + + @staticmethod + def cache_request_packet(packet): + if len(packet.data) == RNS.Identity.HASHLENGTH/8: + packet = Transport.get_cached_packet(packet.data) + + if packet != None: + # If the packet was retrieved from the local + # cache, replay it to the Transport instance, + # so that it can be directed towards it original + # destination. + Transport.inbound(packet.raw, packet.receiving_interface) + return True + else: + return False + else: + return False + + @staticmethod + def cache_request(packet_hash, destination): + cached_packet = Transport.get_cached_packet(packet_hash) + if cached_packet: + # The packet was found in the local cache, + # replay it to the Transport instance. + Transport.inbound(packet.raw, packet.receiving_interface) + else: + # The packet is not in the local cache, + # query the network. + RNS.Packet(destination, packet_hash, context = RNS.Packet.CACHE_REQUEST).send() + + @staticmethod + def hasPath(destination_hash): + if destination_hash in Transport.destination_table: + return True + else: + return False + + @staticmethod + def requestPath(destination_hash): + path_request_data = destination_hash + RNS.Identity.getRandomHash() + path_request_dst = RNS.Destination(None, RNS.Destination.OUT, RNS.Destination.PLAIN, Transport.APP_NAME, "path", "request") + packet = RNS.Packet(path_request_dst, path_request_data, packet_type = RNS.Packet.DATA, transport_type = RNS.Transport.BROADCAST, header_type = RNS.Packet.HEADER_1) + packet.send() + + @staticmethod + def requestPathOnInterface(destination_hash, interface): + path_request_data = destination_hash + RNS.Identity.getRandomHash() + path_request_dst = RNS.Destination(None, RNS.Destination.OUT, RNS.Destination.PLAIN, Transport.APP_NAME, "path", "request") + packet = RNS.Packet(path_request_dst, path_request_data, packet_type = RNS.Packet.DATA, transport_type = RNS.Transport.BROADCAST, header_type = RNS.Packet.HEADER_1, attached_interface = interface) + packet.send() + + @staticmethod + def path_request_handler(data, packet): + if len(data) >= RNS.Identity.TRUNCATED_HASHLENGTH//8: + Transport.pathRequest( + data[:RNS.Identity.TRUNCATED_HASHLENGTH//8], + Transport.from_local_client(packet), + packet.receiving_interface + ) + + @staticmethod + def pathRequest(destination_hash, is_from_local_client, attached_interface): + RNS.log("Path request for "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG) + + local_destination = next((d for d in Transport.destinations if d.hash == destination_hash), None) + if local_destination != None: + RNS.log("Destination is local to this system, announcing", RNS.LOG_DEBUG) + local_destination.announce(path_response=True) + + elif (RNS.Reticulum.transport_enabled() or is_from_local_client) and destination_hash in Transport.destination_table: + RNS.log("Path found, inserting announce for transmission", RNS.LOG_DEBUG) + packet = Transport.destination_table[destination_hash][6] + received_from = Transport.destination_table[destination_hash][5] + + now = time.time() + retries = Transport.PATHFINDER_R + local_rebroadcasts = 0 + block_rebroadcasts = True + announce_hops = packet.hops + + if is_from_local_client: + retransmit_timeout = now + else: + # TODO: Look at this timing + retransmit_timeout = now + Transport.PATH_REQUEST_GRACE # + (RNS.rand() * Transport.PATHFINDER_RW) + + # This handles an edge case where a peer sends a past + # request for a destination just after an announce for + # said destination has arrived, but before it has been + # rebroadcast locally. In such a case the actual announce + # is temporarily held, and then reinserted when the path + # request has been served to the peer. + if packet.destination_hash in Transport.announce_table: + held_entry = Transport.announce_table[packet.destination_hash] + Transport.held_announces[packet.destination_hash] = held_entry + + Transport.announce_table[packet.destination_hash] = [now, retransmit_timeout, retries, received_from, announce_hops, packet, local_rebroadcasts, block_rebroadcasts, attached_interface] + + elif is_from_local_client: + # Forward path request on all interfaces + # except the local client + for interface in Transport.interfaces: + if not interface == attached_interface: + Transport.requestPathOnInterface(destination_hash, interface) + + elif not is_from_local_client and len(Transport.local_client_interfaces) > 0: + # Forward the path request on all local + # client interfaces + for interface in Transport.local_client_interfaces: + Transport.requestPathOnInterface(destination_hash, interface) + + else: + RNS.log("No known path to requested destination, ignoring request", RNS.LOG_DEBUG) + + @staticmethod + def from_local_client(packet): + if hasattr(packet.receiving_interface, "parent_interface"): + return Transport.is_local_client_interface(packet.receiving_interface) + else: + return False + + @staticmethod + def is_local_client_interface(interface): + if hasattr(interface, "parent_interface"): + if hasattr(interface.parent_interface, "is_local_shared_instance"): + return True + else: + return False + else: + return False + + @staticmethod + def interface_to_shared_instance(interface): + if hasattr(interface, "is_connected_to_shared_instance"): + return True + else: + return False + + @staticmethod + def exitHandler(): + RNS.log("Saving packet hashlist to storage...", RNS.LOG_VERBOSE) + try: + packet_hashlist_path = RNS.Reticulum.storagepath+"/packet_hashlist" + file = open(packet_hashlist_path, "wb") + file.write(umsgpack.packb(Transport.packet_hashlist)) + file.close() + RNS.log("Done packet hashlist to storage", RNS.LOG_VERBOSE) + except Exception as e: + RNS.log("Could not save packet hashlist to storage, the contained exception was: "+str(e), RNS.LOG_ERROR) + + if not Transport.owner.is_connected_to_shared_instance: + RNS.log("Saving path table to storage...", RNS.LOG_VERBOSE) + try: + serialised_destinations = [] + for destination_hash in Transport.destination_table: + # Get the destination entry from the destination table + de = Transport.destination_table[destination_hash] + interface_hash = de[5].get_hash() + + # Only store destination table entry if the associated + # interface is still active + interface = Transport.find_interface_from_hash(interface_hash) + if interface != None: + # Get the destination entry from the destination table + de = Transport.destination_table[destination_hash] + timestamp = de[0] + received_from = de[1] + hops = de[2] + expires = de[3] + random_blobs = de[4] + packet_hash = de[6].getHash() + + serialised_entry = [ + destination_hash, + timestamp, + received_from, + hops, + expires, + random_blobs, + interface_hash, + packet_hash + ] + + serialised_destinations.append(serialised_entry) + + Transport.cache(de[6], force_cache=True) + + destination_table_path = RNS.Reticulum.storagepath+"/destination_table" + file = open(destination_table_path, "wb") + file.write(umsgpack.packb(serialised_destinations)) + file.close() + RNS.log("Done saving path table to storage", RNS.LOG_VERBOSE) + except Exception as e: + RNS.log("Could not save path table to storage, the contained exception was: "+str(e), RNS.LOG_ERROR) diff --git a/RNS/__init__.py b/RNS/__init__.py index 033e20d..dcbfc3d 100755 --- a/RNS/__init__.py +++ b/RNS/__init__.py @@ -25,10 +25,10 @@ LOG_VERBOSE = 5 LOG_DEBUG = 6 LOG_EXTREME = 7 -LOG_STDOUT = 0x91 +LOG_STDOUT = 0x91 LOG_FILE = 0x92 -loglevel = LOG_NOTICE +loglevel = LOG_NOTICE logfile = None logdest = LOG_STDOUT logtimefmt = "%Y-%m-%d %H:%M:%S" @@ -36,54 +36,54 @@ logtimefmt = "%Y-%m-%d %H:%M:%S" random.seed(os.urandom(10)) def loglevelname(level): - if (level == LOG_CRITICAL): - return "Critical" - if (level == LOG_ERROR): - return "Error" - if (level == LOG_WARNING): - return "Warning" - if (level == LOG_NOTICE): - return "Notice" - if (level == LOG_INFO): - return "Info" - if (level == LOG_VERBOSE): - return "Verbose" - if (level == LOG_DEBUG): - return "Debug" - if (level == LOG_EXTREME): - return "Extra" - - return "Unknown" + if (level == LOG_CRITICAL): + return "Critical" + if (level == LOG_ERROR): + return "Error" + if (level == LOG_WARNING): + return "Warning" + if (level == LOG_NOTICE): + return "Notice" + if (level == LOG_INFO): + return "Info" + if (level == LOG_VERBOSE): + return "Verbose" + if (level == LOG_DEBUG): + return "Debug" + if (level == LOG_EXTREME): + return "Extra" + + return "Unknown" def log(msg, level=3): - # TODO: not thread safe - if loglevel >= level: - timestamp = time.time() - logstring = "["+time.strftime(logtimefmt)+"] ["+loglevelname(level)+"] "+msg + # TODO: not thread safe + if loglevel >= level: + timestamp = time.time() + logstring = "["+time.strftime(logtimefmt)+"] ["+loglevelname(level)+"] "+msg - if (logdest == LOG_STDOUT): - print(logstring) + if (logdest == LOG_STDOUT): + print(logstring) - if (logdest == LOG_FILE and logfile != None): - file = open(logfile, "a") - file.write(logstring+"\n") - file.close() + if (logdest == LOG_FILE and logfile != None): + file = open(logfile, "a") + file.write(logstring+"\n") + file.close() def rand(): - result = random.random() - return result + result = random.random() + return result def hexrep(data, delimit=True): - delimiter = ":" - if not delimit: - delimiter = "" - hexrep = delimiter.join("{:02x}".format(c) for c in data) - return hexrep + delimiter = ":" + if not delimit: + delimiter = "" + hexrep = delimiter.join("{:02x}".format(c) for c in data) + return hexrep def prettyhexrep(data): - delimiter = "" - hexrep = "<"+delimiter.join("{:02x}".format(c) for c in data)+">" - return hexrep + delimiter = "" + hexrep = "<"+delimiter.join("{:02x}".format(c) for c in data)+">" + return hexrep def panic(): - os._exit(255) \ No newline at end of file + os._exit(255) \ No newline at end of file