diff --git a/RNS/Interfaces/AX25KISSInterface.py b/RNS/Interfaces/AX25KISSInterface.py index addd285..cb35ed0 100644 --- a/RNS/Interfaces/AX25KISSInterface.py +++ b/RNS/Interfaces/AX25KISSInterface.py @@ -301,7 +301,10 @@ class AX25KISSInterface(Interface): 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) + RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR) + + if RNS.Reticulum.panic_on_interface_error: + RNS.panic() def __str__(self): return "AX25KISSInterface["+self.name+"]" \ No newline at end of file diff --git a/RNS/Interfaces/KISSInterface.py b/RNS/Interfaces/KISSInterface.py index a4c508c..635bc2c 100644 --- a/RNS/Interfaces/KISSInterface.py +++ b/RNS/Interfaces/KISSInterface.py @@ -277,7 +277,10 @@ class KISSInterface(Interface): 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) + RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR) + + if RNS.Reticulum.panic_on_interface_error: + RNS.panic() def __str__(self): return "KISSInterface["+self.name+"]" \ No newline at end of file diff --git a/RNS/Interfaces/LocalInterface.py b/RNS/Interfaces/LocalInterface.py index c187f4f..f51a8ba 100644 --- a/RNS/Interfaces/LocalInterface.py +++ b/RNS/Interfaces/LocalInterface.py @@ -127,6 +127,10 @@ class LocalClientInterface(Interface): if self in RNS.Transport.local_client_interfaces: RNS.Transport.local_client_interfaces.remove(self) + RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR) + if RNS.Reticulum.panic_on_interface_error: + RNS.panic() + def __str__(self): return "LocalInterface["+str(self.target_port)+"]" diff --git a/RNS/Interfaces/RNodeInterface.py b/RNS/Interfaces/RNodeInterface.py index 13be2b9..e99b24f 100644 --- a/RNS/Interfaces/RNodeInterface.py +++ b/RNS/Interfaces/RNodeInterface.py @@ -463,7 +463,10 @@ class RNodeInterface(Interface): 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) + RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR) + + if RNS.Reticulum.panic_on_interface_error: + RNS.panic() def __str__(self): return "RNodeInterface["+self.name+"]" diff --git a/RNS/Interfaces/SerialInterface.py b/RNS/Interfaces/SerialInterface.py index d33ec12..a131153 100755 --- a/RNS/Interfaces/SerialInterface.py +++ b/RNS/Interfaces/SerialInterface.py @@ -130,7 +130,10 @@ class SerialInterface(Interface): 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) + RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR) + + if RNS.Reticulum.panic_on_interface_error: + RNS.panic() def __str__(self): return "SerialInterface["+self.name+"]" diff --git a/RNS/Interfaces/TCPInterface.py b/RNS/Interfaces/TCPInterface.py index 0eefac0..bc33fcc 100644 --- a/RNS/Interfaces/TCPInterface.py +++ b/RNS/Interfaces/TCPInterface.py @@ -23,13 +23,21 @@ class ThreadingTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): pass class TCPClientInterface(Interface): + RECONNECT_WAIT = 5 + RECONNECT_MAX_TRIES = None - def __init__(self, owner, name, target_ip=None, target_port=None, connected_socket=None): + def __init__(self, owner, name, target_ip=None, target_port=None, connected_socket=None, max_reconnect_tries=None): self.IN = True self.OUT = False self.socket = None self.parent_interface = None self.name = name + self.initiator = False + + if max_reconnect_tries == None: + self.max_reconnect_tries = TCPClientInterface.RECONNECT_MAX_TRIES + else: + self.max_reconnect_tries = max_reconnect_tries if connected_socket != None: self.receives = True @@ -50,11 +58,43 @@ class TCPClientInterface(Interface): self.writing = False if connected_socket == None: + self.initiator = True thread = threading.Thread(target=self.read_loop) thread.setDaemon(True) thread.start() self.wants_tunnel = True + def reconnect(self): + if self.initiator: + attempts = 0 + while not self.online: + attempts += 1 + + if self.max_reconnect_tries != None and attempts > self.max_reconnect_tries: + RNS.log("Max reconnection attempts reached for "+str(self), RNS.LOG_ERROR) + self.teardown() + break + + try: + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.connect((self.target_ip, self.target_port)) + self.online = True + self.writing = False + + thread = threading.Thread(target=self.read_loop) + thread.setDaemon(True) + thread.start() + RNS.Transport.synthesize_tunnel(self) + + except Exception as e: + RNS.log("Reconnection attempt for "+str(self)+" failed. The contained exception was: "+str(e), RNS.LOG_ERROR) + + time.sleep(TCPClientInterface.RECONNECT_WAIT) + + else: + RNS.log("Attempt to reconnect on a non-initiator TCP interface. This should not happen.", RNS.LOG_ERROR) + raise IOError("Attempt to reconnect on a non-initiator TCP interface") + def processIncoming(self, data): self.owner.inbound(data, self) @@ -105,24 +145,30 @@ class TCPClientInterface(Interface): escape = False data_buffer = data_buffer+bytes([byte]) else: - RNS.log("TCP socket for "+str(self)+" was closed, tearing down interface", RNS.LOG_VERBOSE) - self.teardown() + RNS.log("TCP socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING) + self.online = False + if self.initiator: + self.reconnect() + break except Exception as e: self.online = False RNS.log("An interface error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) - RNS.log("Tearing down "+str(self), RNS.LOG_ERROR) self.teardown() def teardown(self): + RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR) self.online = False self.OUT = False self.IN = False if self in RNS.Transport.interfaces: RNS.Transport.interfaces.remove(self) + if RNS.Reticulum.panic_on_interface_error: + RNS.panic() + def __str__(self): return "TCPInterface["+str(self.name)+"/"+str(self.target_ip)+":"+str(self.target_port)+"]" diff --git a/RNS/Reticulum.py b/RNS/Reticulum.py index da4173b..f79366e 100755 --- a/RNS/Reticulum.py +++ b/RNS/Reticulum.py @@ -98,6 +98,8 @@ class Reticulum: Reticulum.__transport_enabled = False Reticulum.__use_implicit_proof = True + Reticulum.panic_on_interface_error = False + self.local_interface_port = 37428 self.share_instance = True @@ -195,6 +197,10 @@ class Reticulum: v = self.config["reticulum"].as_bool(option) if v == True: Reticulum.__transport_enabled = True + if option == "panic_on_interface_error": + v = self.config["reticulum"].as_bool(option) + if v == True: + Reticulum.panic_on_interface_error = True if option == "use_implicit_proof": v = self.config["reticulum"].as_bool(option) if v == True: @@ -510,6 +516,14 @@ share_instance = Yes shared_instance_port = 37428 +# You can configure Reticulum to panic and forcibly close +# if an unrecoverable interface error occurs, such as the +# hardware device for an interface disappearing. This is +# an optional directive, and can be left out for brevity. +# This behaviour is disabled by default. + +panic_on_interface_error = No + [logging] # Valid log levels are 0 through 7: