Added live interface attach/detach/reload

This commit is contained in:
Mark Qvist 2026-09-27 21:42:56 +02:00
parent 84709ccf09
commit d23261c8d9
No known key found for this signature in database
4 changed files with 170 additions and 22 deletions

View file

@ -1,6 +1,6 @@
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist
# Copyright (c) 2016-2026 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@ -257,6 +257,7 @@ class Reticulum:
Reticulum.__local_hops_delta = False
Reticulum.__link_mtu_discovery = Reticulum.LINK_MTU_DISCOVERY
Reticulum.__remote_management_enabled = False
Reticulum.__interface_management_enabled = True
Reticulum.__use_implicit_proof = True
Reticulum.__allow_probes = False
Reticulum.__discovery_enabled = False
@ -548,6 +549,11 @@ class Reticulum:
v = self.config["reticulum"].as_bool(option)
if v == True: Reticulum.__remote_management_enabled = True
if option == "enable_interface_management":
v = self.config["reticulum"].as_bool(option)
if v == False: Reticulum.__interface_management_enabled = False
else: Reticulum.__interface_management_enabled = True
if option == "remote_management_allowed":
v = self.config["reticulum"].as_list(option)
for hexhash in v:
@ -757,7 +763,79 @@ class Reticulum:
RNS.log("System interfaces are ready", RNS.LOG_VERBOSE)
def _synthesize_interface(self, config, name, instance_init=False):
def _attach_interface(self, name):
if not Reticulum.__interface_management_enabled: return False
try:
interfaces = { iface.name: iface for iface in RNS.Transport.interfaces }
if name in interfaces:
RNS.log(f"Attempt to attach existing interface \"{name}\"", RNS.LOG_WARNING)
return False
else:
try: config = ConfigObj(self.configpath)
except Exception as e:
RNS.log(f"Could not parse the configuration at {self.configpath} during interface attach", RNS.LOG_ERROR)
return None
if "interfaces" in config:
if not name in config["interfaces"]:
RNS.log(f"Cannot attach interface \"{name}\", no configuration entry exists", RNS.LOG_ERROR)
return None
else:
interface_config = config["interfaces"][name]
self._synthesize_interface(interface_config, name, force_attach=True)
RNS.log(f"Interface \"{name}\" was attached", RNS.LOG_NOTICE)
return True
except Exception as e: RNS.log(f"Error while detaching interface \"{name}\": {e}", RNS.LOG_ERROR)
return False
def _detach_interface(self, name):
if not Reticulum.__interface_management_enabled: return False
try:
interfaces = { iface.name: iface for iface in RNS.Transport.interfaces }
if not name in interfaces:
RNS.log(f"Attempt to detach non-existing interface \"{name}\"", RNS.LOG_WARNING)
return None
else:
interface = interfaces[name]
if isinstance(interface, I2PInterface.I2PInterface): return False
if isinstance(interface, LocalInterface.LocalClientInterface): return False
if isinstance(interface, LocalInterface.LocalServerInterface): return False
if interface.spawned_interfaces:
if type(interface.spawned_interfaces) == dict: spawned = list(interface.spawned_interfaces.values())
elif type(interface.spawned_interfaces) == list: spawned = interface.spawned_interfaces.copy()
for s in spawned:
s.detach()
s.teardown()
RNS.Transport.remove_interface(s)
interface.detach()
RNS.Transport.remove_interface(interface)
RNS.log(f"Interface \"{name}\" was detached", RNS.LOG_NOTICE)
return True
except Exception as e: RNS.log(f"Error while detaching interface \"{name}\": {e}", RNS.LOG_ERROR); RNS.trace_exception(e)
return False
def _reload_interface(self, name):
if not Reticulum.__interface_management_enabled: return False
interfaces = { iface.name: iface for iface in RNS.Transport.interfaces }
if not name in interfaces:
RNS.log(f"Attempt to reload non-existing interface \"{name}\"", RNS.LOG_WARNING)
return None
if self._detach_interface(name) and self._attach_interface(name):
RNS.log(f"Interface \"{name}\" was reloaded", RNS.LOG_NOTICE)
return True
else:
RNS.log(f"Could not reload interface \"{name}\"", RNS.LOG_NOTICE)
return False
def _synthesize_interface(self, config, name, instance_init=False, force_attach=False):
c = config
interface_mode = Interface.Interface.MODE_FULL
@ -1009,7 +1087,7 @@ class Reticulum:
interface.final_init()
interface = None
if (("interface_enabled" in c) and c.as_bool("interface_enabled") == True) or (("enabled" in c) and c.as_bool("enabled") == True):
if (("interface_enabled" in c) and c.as_bool("interface_enabled") == True) or (("enabled" in c) and c.as_bool("enabled") == True) or force_attach:
interface_config = c
interface_config["name"] = name
interface_config["selected_interface_mode"] = interface_mode
@ -1113,7 +1191,6 @@ class Reticulum:
except Exception as e:
RNS.log(f"External interface initialisation failed for {interface_type} / {name}", RNS.LOG_ERROR)
RNS.trace_exception(e)
else:
RNS.log("Skipping disabled interface \""+name+"\"", RNS.LOG_DEBUG)
@ -1300,6 +1377,12 @@ class Reticulum:
if path == "blackholed_identities": self.rpc_return(conn, self.get_blackholed_identities())
if path == "is_blackholed": self.rpc_return(conn, self.is_blackholed(call["identity_hash"]))
if "manage" in call:
path = call["manage"]
if path == "attach_interface": self.rpc_return(conn, self.attach_interface(call["name"]))
if path == "detach_interface": self.rpc_return(conn, self.detach_interface(call["name"]))
if path == "reload_interface": self.rpc_return(conn, self.reload_interface(call["name"]))
if "drop" in call:
path = call["drop"]
if path == "path": self.rpc_return(conn, self.drop_path(call["destination_hash"]))
@ -1867,21 +1950,39 @@ class Reticulum:
if RNS.Profiler.ran(): return RNS.Profiler.results()
else: return None
def halt_interface(self, interface):
pass
def attach_interface(self, interface_name):
if self.is_connected_to_shared_instance:
rpc_connection = self.get_rpc_client()
rpc_connection.send_bytes(mp.packb({"manage": "attach_interface", "name": interface_name}))
response = mp.unpackb(rpc_connection.recv_bytes())
return response
else: return self._attach_interface(interface_name)
def resume_interface(self, interface):
pass
def detach_interface(self, interface_name):
if self.is_connected_to_shared_instance:
rpc_connection = self.get_rpc_client()
rpc_connection.send_bytes(mp.packb({"manage": "detach_interface", "name": interface_name}))
response = mp.unpackb(rpc_connection.recv_bytes())
return response
else: return self._detach_interface(interface_name)
def reload_interface(self, interface):
pass
def reload_interface(self, interface_name):
if self.is_connected_to_shared_instance:
rpc_connection = self.get_rpc_client()
rpc_connection.send_bytes(mp.packb({"manage": "reload_interface", "name": interface_name}))
response = mp.unpackb(rpc_connection.recv_bytes())
return response
else: return self._reload_interface(interface_name)
def get_blackholed_identities(self):
if self.is_connected_to_shared_instance:
rpc_connection = self.get_rpc_client()
rpc_connection.send_bytes(mp.packb({"get": "blackholed_identities"}))
response = mp.unpackb(rpc_connection.recv_bytes())
return response
rpc_connection = self.get_rpc_client()
rpc_connection.send_bytes(mp.packb({"get": "blackholed_identities"}))
response = mp.unpackb(rpc_connection.recv_bytes())
return response
else: return RNS.Transport.blackholed_identities
@ -1892,10 +1993,10 @@ class Reticulum:
if len(identity_hash) != RNS.Reticulum.TRUNCATED_HASHLENGTH//8: raise ValueError("Invalid identity hash length for blackhole check")
if self.is_connected_to_shared_instance:
rpc_connection = self.get_rpc_client()
rpc_connection.send_bytes(mp.packb({"get": "is_blackholed", "identity_hash": identity_hash}))
response = mp.unpackb(rpc_connection.recv_bytes())
return response
rpc_connection = self.get_rpc_client()
rpc_connection.send_bytes(mp.packb({"get": "is_blackholed", "identity_hash": identity_hash}))
response = mp.unpackb(rpc_connection.recv_bytes())
return response
else: return identity_hash in RNS.Transport.blackholed_identities

View file

@ -215,6 +215,14 @@ instance_name = default
# required_discovery_value = 14
# You can disable allowing interfaces to be attached, de-
# tached or reloaded via rnstatus, for example if running
# on a shared system where users with normal RPC access
# should not be allowed to control interface states.
# enable_interface_management = no
# 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

View file

@ -159,7 +159,8 @@ def get_remote_status(destination_hash, include_lstats, include_profiling, ident
def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=False, astats=False, pstats=False, lstats=False,
sorting=None, sort_reverse=False, remote=None, management_identity=None, must_exit=True, rns_instance=None,
traffic_totals=False, discovered_interfaces=False, config_entries=False, burst_filter=False, blocked_ips=False,
queue_stats=False, pps=False, profiling=False, remote_timeout=RNS.Transport.PATH_REQUEST_TIMEOUT):
queue_stats=False, pps=False, profiling=False, remote_timeout=RNS.Transport.PATH_REQUEST_TIMEOUT,
attach=None, detach=None, reload=None):
if remote: require_shared = False
else: require_shared = True
@ -176,6 +177,37 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
if must_exit: exit(1)
else: return
if attach:
result = reticulum.attach_interface(attach)
if result == True: print(f"Interface {attach} was attached")
elif result == False: print(f"Could not attach interface {attach}")
elif result == None: print(f"The interface {attach} does not exist")
else: print(f"Unknown error while attaching interface {attach}")
if result == True: exit(0)
else: exit(1)
if detach:
result = reticulum.detach_interface(detach)
if result == True: print(f"Interface {detach} was detached")
elif result == False: print(f"Could not detach interface {detach}")
elif result == None: print(f"The interface {detach} does not exist")
else: print(f"Unknown error while detaching interface {detach}")
if result == True: exit(0)
else: exit(1)
if reload:
result = reticulum.reload_interface(reload)
if result == True: print(f"Interface {reload} was reloaded")
elif result == False: print(f"Could not reload interface {reload}")
elif result == None: print(f"The interface {reload} does not exist")
else: print(f"Unknown error while reloading interface {reload}")
if result == True: exit(0)
else: exit(1)
link_count = None
active_link_count = None
stats = None
@ -832,6 +864,10 @@ def main(must_exit=True, rns_instance=None):
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
parser.add_argument("--version", action="version", version="rnstatus {version}".format(version=__version__))
parser.add_argument("--attach", action="store", metavar="name", help="Attach interface by name", default=None, type=str)
parser.add_argument("--detach", action="store", metavar="name", help="Detach interface by name", default=None, type=str)
parser.add_argument("--reload", action="store", metavar="name", help="Reload interface by name", default=None, type=str)
parser.add_argument("-a", "--all", action="store_true", help="show all interfaces", default=False)
parser.add_argument("-A", "--announce-stats", action="store_true", help="show announce stats", default=False)
parser.add_argument("-P", "--pr-stats", action="store_true", help="show path request stats", default=False)
@ -898,7 +934,8 @@ def main(must_exit=True, rns_instance=None):
astats=args.announce_stats, pstats=args.pr_stats, lstats=args.link_stats, sorting=args.sort, sort_reverse=args.reverse,
remote=args.R, management_identity=args.i, remote_timeout=args.w, must_exit=must_exit, rns_instance=rns_instance,
traffic_totals=args.totals, discovered_interfaces=args.discovered, config_entries=args.D, burst_filter=args.burst,
blocked_ips=args.blocked_ips, queue_stats=args.queues, pps=args.pps, profiling=args.profiling)
blocked_ips=args.blocked_ips, queue_stats=args.queues, pps=args.pps, profiling=args.profiling,
attach=args.attach, detach=args.detach, reload=args.reload)
except KeyboardInterrupt:
print("")

View file

@ -274,8 +274,7 @@ You can easily add ``rnsd`` as an always-on service by :ref:`configuring a servi
The rnstatus Utility
====================
Using the ``rnstatus`` utility, you can view the status of configured Reticulum
interfaces, similar to the ``ifconfig`` program.
Using the ``rnstatus`` utility, you can view the status of and control configured Reticulum interfaces, similar to the ``ifconfig`` program.
**Usage Examples**
@ -350,6 +349,9 @@ Filter output to only show some interfaces:
-h, --help show this help message and exit
--config CONFIG path to alternative Reticulum config directory
--version show program's version number and exit
--attach name Attach interface by name
--detach name Detach interface by name
--reload name Reload interface by name
-a, --all show all interfaces
-A, --announce-stats show announce stats
-P, --pr-stats show path request stats