Updated rnsh config args to work similarly to other RNS utils

This commit is contained in:
Mark Qvist
2026-07-19 00:57:27 +02:00
parent 5081255317
commit ec8d43a548
5 changed files with 87 additions and 65 deletions
+2 -1
View File
@@ -44,7 +44,8 @@ def setup_argument_parser():
parser = argparse.ArgumentParser(description="Reticulum Remote Shell Utility", epilog="When specifying a command to execute, separate rnsh\noptions from the command and its arguments with --\n\nFor example:\n rnsh -l -- /bin/bash --login\n rnsh <destination> -- ls -la /tmp", formatter_class=argparse.RawDescriptionHelpFormatter) parser = argparse.ArgumentParser(description="Reticulum Remote Shell Utility", epilog="When specifying a command to execute, separate rnsh\noptions from the command and its arguments with --\n\nFor example:\n rnsh -l -- /bin/bash --login\n rnsh <destination> -- ls -la /tmp", formatter_class=argparse.RawDescriptionHelpFormatter)
# Common options # Common options
parser.add_argument("--config", "-c", action="store", default=None, help="path to alternative Reticulum config directory", type=str) parser.add_argument("--config", "-c", action="store", default=None, help="path to config directory", type=str)
parser.add_argument("--rnsconfig", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
parser.add_argument("--identity", "-i", action="store", default=None, help="path to identity file to use", type=str) parser.add_argument("--identity", "-i", action="store", default=None, help="path to identity file to use", type=str)
parser.add_argument("-v", "--verbose", action="count", default=0, help="increase verbosity") parser.add_argument("-v", "--verbose", action="count", default=0, help="increase verbosity")
parser.add_argument("-q", "--quiet", action="count", default=0, help="decrease verbosity") parser.add_argument("-q", "--quiet", action="count", default=0, help="decrease verbosity")
+5 -4
View File
@@ -144,7 +144,7 @@ class RemoteExecutionError(Exception):
def __init__(self, msg): self.msg = msg def __init__(self, msg): self.msg = msg
async def _initiate_link(configdir, rnsconfigdir, identitypath=None, verbosity=0, quietness=0, noid=False, destination=None, async def _initiate_link(configdir, rnsconfigdir, identitypath=None, logfile=None, verbosity=0, quietness=0, noid=False, destination=None,
timeout=RNS.Transport.PATH_REQUEST_TIMEOUT): timeout=RNS.Transport.PATH_REQUEST_TIMEOUT):
global _identity, _reticulum, _link, _destination, _remote_exec_grace global _identity, _reticulum, _link, _destination, _remote_exec_grace
@@ -160,11 +160,11 @@ async def _initiate_link(configdir, rnsconfigdir, identitypath=None, verbosity=0
if _reticulum is None: if _reticulum is None:
targetloglevel = compute_target_rns_loglevel(verbosity, quietness, RNS.LOG_ERROR) targetloglevel = compute_target_rns_loglevel(verbosity, quietness, RNS.LOG_ERROR)
RNS.logfile = os.path.join(configdir, "logfile") RNS.logfile = logfile
_reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel, logdest=RNS.LOG_FILE) _reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel, logdest=RNS.LOG_FILE)
if _identity is None: if _identity is None:
_identity = rnsh.prepare_identity(identitypath) _identity = rnsh.prepare_identity(identity_path=identitypath, service_name=None, configdir=configdir)
if not RNS.Transport.has_path(destination_hash): if not RNS.Transport.has_path(destination_hash):
RNS.Transport.request_path(destination_hash) RNS.Transport.request_path(destination_hash)
@@ -211,8 +211,9 @@ async def _handle_error(errmsg: RNS.MessageBase):
raise RemoteExecutionError(f"Remote error: {errmsg.msg}") raise RemoteExecutionError(f"Remote error: {errmsg.msg}")
async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, verbosity: int, quietness: int, noid: bool, destination: str, async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, logfile:str, verbosity: int, quietness: int, noid: bool, destination: str,
timeout: float, command: [str] | None = None): timeout: float, command: [str] | None = None):
global _finished, _link global _finished, _link
if timeout is None: if timeout is None:
timeout = RNS.Transport.PATH_REQUEST_TIMEOUT timeout = RNS.Transport.PATH_REQUEST_TIMEOUT
+12 -5
View File
@@ -118,25 +118,32 @@ def compute_target_rns_loglevel(verbosity: int, quietness: int, base_level: int
except Exception: return base_level except Exception: return base_level
async def listen(configdir, rnsconfigdir, command, identitypath=None, service_name=None, verbosity=0, quietness=0, allowed=None, async def listen(configdir, rnsconfigdir, command, identitypath=None, logfile=None, service_name=None, verbosity=0, quietness=0,
allowed_file=None, disable_auth=None, announce_period=900, no_remote_command=True, remote_cmd_as_args=False, allowed=None, allowed_file=None, disable_auth=None, announce_period=900, no_remote_command=True, remote_cmd_as_args=False,
loop: asyncio.AbstractEventLoop = None): loop: asyncio.AbstractEventLoop = None):
global _identity, _allow_all, _allowed_identity_hashes, _allowed_file, _allowed_file_identity_hashes global _identity, _allow_all, _allowed_identity_hashes, _allowed_file, _allowed_file_identity_hashes
global _reticulum, _cmd, _destination, _no_remote_command, _remote_cmd_as_args, _finished global _reticulum, _cmd, _destination, _no_remote_command, _remote_cmd_as_args, _finished
if not loop: loop = asyncio.get_running_loop() if not loop: loop = asyncio.get_running_loop()
if service_name is None or len(service_name) == 0: if service_name is None or len(service_name) == 0: service_name = "default"
service_name = "default"
RNS.log(f"Using service name {service_name}", RNS.LOG_INFO) RNS.log(f"Using service name {service_name}", RNS.LOG_INFO)
if logfile:
RNS.log(f"Logging to {logfile}", RNS.LOG_NOTICE)
logdest = RNS.LOG_FILE
RNS.logfile = logfile
else:
RNS.log(f"Logging to console", RNS.LOG_NOTICE)
logdest = RNS.LOG_STDOUT
# More -v should increase verbosity (higher RNS.loglevel); -q should decrease it # More -v should increase verbosity (higher RNS.loglevel); -q should decrease it
targetloglevel = compute_target_rns_loglevel(verbosity, quietness, RNS.LOG_INFO) targetloglevel = compute_target_rns_loglevel(verbosity, quietness, RNS.LOG_INFO)
_reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel) _reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel)
_identity = rnsh.prepare_identity(identitypath, service_name) _identity = rnsh.prepare_identity(identity_path=identitypath, service_name=service_name, configdir=configdir)
_destination = RNS.Destination(_identity, RNS.Destination.IN, RNS.Destination.SINGLE, rnsh.APP_NAME) _destination = RNS.Destination(_identity, RNS.Destination.IN, RNS.Destination.SINGLE, rnsh.APP_NAME)
RNS.log(f"rnsh listening for commands on {RNS.prettyhexrep(_destination.hash)}", RNS.LOG_NOTICE) RNS.log(f"rnsh listening for commands on {RNS.prettyhexrep(_destination.hash)}", RNS.LOG_NOTICE)
RNS.logdest = logdest
_cmd = command _cmd = command
if _cmd is None or len(_cmd) == 0: if _cmd is None or len(_cmd) == 0:
+40 -23
View File
@@ -57,43 +57,54 @@ loop: asyncio.AbstractEventLoop | None = None
def _sanitize_service_name(service_name:str) -> str: return re.sub(r'\W+', '', service_name) def _sanitize_service_name(service_name:str) -> str: return re.sub(r'\W+', '', service_name)
def prepare_identity(identity_path, service_name: str = None) -> tuple[RNS.Identity]: def prepare_identity(identity_path=None, service_name=None, configdir=None):
service_name = _sanitize_service_name(service_name or "") service_name = _sanitize_service_name(service_name or "")
if identity_path is None: if not identity_path:
identity_path = RNS.Reticulum.identitypath + "/" + APP_NAME + \ identity_path = f"{configdir}/identity" + (f".{service_name}" if service_name and len(service_name) > 0 else "")
(f".{service_name}" if service_name and len(service_name) > 0 else "")
identity = None identity = None
if os.path.isfile(identity_path): if os.path.isfile(identity_path):
RNS.log(f"Loading identity from {identity_path}", RNS.LOG_VERBOSE)
identity = RNS.Identity.from_file(identity_path) identity = RNS.Identity.from_file(identity_path)
if identity is None: if identity is None:
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO) RNS.log(f"Creating identity {identity_path}", RNS.LOG_NOTICE)
identity = RNS.Identity() identity = RNS.Identity()
identity.to_file(identity_path) identity.to_file(identity_path)
return identity return identity
def print_identity(configdir, identitypath, service_name, include_destination: bool): def print_identity(configdir, rnsconfigdir, identitypath, service_name, include_destination: bool):
reticulum = RNS.Reticulum(configdir=configdir, loglevel=RNS.LOG_INFO) reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=RNS.LOG_INFO)
if service_name and len(service_name) > 0: if service_name and len(service_name) > 0: print(f"Using service name \"{service_name}\"")
print(f"Using service name \"{service_name}\"") identity = prepare_identity(identity_path=identitypath, service_name=service_name, configdir=configdir)
identity = prepare_identity(identitypath, service_name)
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME) destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME)
print("Identity : " + str(identity)) print("Identity : " + str(identity))
if include_destination: if include_destination: print("Listening on : " + RNS.prettyhexrep(destination.hash))
print("Listening on : " + RNS.prettyhexrep(destination.hash))
exit(0) exit(0)
verbose_set = False verbose_set = False
def ensure_config_directory(): def ensure_config_directory(configdir=None):
if os.path.isdir(os.path.expanduser("~/.config/rnsh")): return os.path.expanduser("~/.config/rnsh") if configdir:
elif os.path.isdir(os.path.expanduser("~/.rnsh")): return os.path.expanduser("~/.rnsh") if os.path.isdir(os.path.expanduser(configdir)): return os.path.expanduser(configdir)
else:
try:
RNS.log(f"Creating configuration directory: {os.path.expanduser(configdir)}")
os.makedirs(os.path.expanduser(configdir))
return os.path.expanduser(configdir)
except Exception as e:
RNS.log(f"Could not get or create rnsh configuration directory, aborting", RNS.LOG_CRITICAL)
os._exit(1)
elif os.path.isdir(os.path.expanduser("~/.config/rnsh")): return os.path.expanduser("~/.config/rnsh")
elif os.path.isdir(os.path.expanduser("~/.rnsh")): return os.path.expanduser("~/.rnsh")
else: else:
try: try:
RNS.log(f"Creating configuration directory: {os.path.expanduser('~/.rnsh')}")
os.makedirs(os.path.expanduser("~/.rnsh")) os.makedirs(os.path.expanduser("~/.rnsh"))
return os.path.expanduser("~/.rnsh") return os.path.expanduser("~/.rnsh")
@@ -106,24 +117,29 @@ async def _rnsh_cli_main():
global verbose_set global verbose_set
args, parser = parse_arguments() args, parser = parse_arguments()
verbose_set = args.verbose > 0 verbose_set = args.verbose > 0
configdir = ensure_config_directory() configdir = ensure_config_directory(args.config)
if not configdir:
RNS.log(f"Could not resolve rnsh configuration directory", RNS.LOG_CRITICAL)
os._exit(1)
if args.print_identity: if args.print_identity:
print_identity(args.config, args.identity, args.service, args.listen) print_identity(configdir, args.rnsconfig, args.identity, args.service, args.listen)
return 0 return 0
logfile = f"{configdir}/logfile"
if args.listen: if args.listen:
allowed_file = None allowed_file = None
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if os.path.isfile(os.path.expanduser("~/.config/rnsh/allowed_identities")): if os.path.isfile(os.path.expanduser(f"{configdir}/allowed_identities")):
allowed_file = os.path.expanduser("~/.config/rnsh/allowed_identities") allowed_file = os.path.expanduser(f"{configdir}/allowed_identities")
elif os.path.isfile(os.path.expanduser("~/.rnsh/allowed_identities")):
allowed_file = os.path.expanduser("~/.rnsh/allowed_identities")
await listener.listen(configdir=configdir, await listener.listen(configdir=configdir,
rnsconfigdir=args.config, rnsconfigdir=args.rnsconfig,
command=args.command, command=args.command,
identitypath=args.identity, identitypath=args.identity,
logfile=logfile,
service_name=args.service, service_name=args.service,
verbosity=args.verbose, verbosity=args.verbose,
quietness=args.quiet, quietness=args.quiet,
@@ -137,8 +153,9 @@ async def _rnsh_cli_main():
if args.destination is not None: if args.destination is not None:
return_code = await initiator.initiate(configdir=configdir, return_code = await initiator.initiate(configdir=configdir,
rnsconfigdir=args.config, rnsconfigdir=args.rnsconfig,
identitypath=args.identity, identitypath=args.identity,
logfile=logfile,
verbosity=args.verbose, verbosity=args.verbose,
quietness=args.quiet, quietness=args.quiet,
noid=args.no_id, noid=args.no_id,
+28 -32
View File
@@ -1002,39 +1002,35 @@ available. These are only recognised immediately after a newline character:
destination hexadecimal hash of the destination to connect to destination hexadecimal hash of the destination to connect to
options: options:
-h, --help show this help message and exit -h, --help show this help message and exit
--config, -c CONFIG path to alternative Reticulum config directory --config, -c PATH path to config directory
--identity, -i IDENTITY --rnsconfig, -c PATH path to alternative Reticulum config directory
path to identity file to use --identity, -i IDENTITY path to identity file to use
-v, --verbose increase verbosity -v, --verbose increase verbosity
-q, --quiet decrease verbosity -q, --quiet decrease verbosity
-p, --print-identity print identity and destination info and exit -p, --print-identity print identity and destination info and exit
--version show program's version number and exit --version show program's version number and exit
-l, --listen listen (server) mode; any command specified after -- -l, --listen listen (server) mode; any command specified after --
will be used as the default command when the initiator will be used as the default command when the initiator
does not provide one or when remote command execution does not provide one or when remote command execution
is disabled; if no command is specified, the default is disabled; if no command is specified, the default
shell of the user running rnsh will be used shell of the user running rnsh will be used
-s, --service SERVICE -s, --service SERVICE service name for identity file if not the default
service name for identity file if not the default -b, --announce PERIOD announce on startup and every PERIOD seconds; specify
-b, --announce PERIOD 0 to announce on startup only
announce on startup and every PERIOD seconds; specify -a, --allowed HASH allow this identity to connect (may be specified
0 to announce on startup only multiple times); allowed identities can also be
-a, --allowed HASH allow this identity to connect (may be specified specified in ~/.rnsh/allowed_identities or
multiple times); allowed identities can also be ~/.config/rnsh/allowed_identities, one hash per line
specified in ~/.rnsh/allowed_identities or -n, --no-auth disable authentication (allow any identity to connect)
~/.config/rnsh/allowed_identities, one hash per line
-n, --no-auth disable authentication (allow any identity to connect)
-A, --remote-command-as-args -A, --remote-command-as-args
concatenate remote command to the argument list of the concatenate remote command to the argument list of the
default program or shell default program or shell
-C, --no-remote-command -C, --no-remote-command disable executing command lines received from the
disable executing command lines received from the remote initiator
remote initiator -N, --no-id disable identity announcement on connect
-N, --no-id disable identity announcement on connect -m, --mirror return with the exit code of the remote process
-m, --mirror return with the exit code of the remote process -w, --timeout SECONDS connect and request timeout in seconds
-w, --timeout SECONDS
connect and request timeout in seconds
When specifying a command to execute, separate rnsh options from the command When specifying a command to execute, separate rnsh options from the command
and its arguments with --. For example: and its arguments with --. For example: