Feature: Nym node autorun CLI (#5916)

* initial commit - add prereqs install script

* add env vars prompt

* automate latest binary url env var

* add install node script

* add modes to nym-node install script

* start main cli framework

* adding branch var for easier deployment and testing

* add systemd config

* add proxy and wss setup script

* add landing page stub and fix nginx script

* add nginx setup

* fix typo

* add checks for existing dir and wg prompt

* add nginx commands

* add service file check

* add service file check

* convention alignment

* add checks to nginx setup

* cleanup old code

* add bonding prompt and nym node run fns

* fix syntax

* fix syntax

* fix syntax

* fix syntax

* fix syntax

* fix syntax

* fix syntax

* fix syntax

* add service script to init

* fix syntax

* fix syntax

* add chmod

* fix script logic

* syntax fix

* syntax fix

* silent mode trial

* fix evn prompt script

* make scripts interactive

* indent fix

* correct node-install script

* initial mixnode setup working - gws need more love

* fix bonding function

* syntax fix

* improve run noide as service script

* improve service script

* improve run service fn

* fix logic

* beautify

* beautify

* create run node as service script

* syntax fix

* attempt to resolve memory running out issue

* attempt to resolve memory running out issue

* attempt to resolve memory running out issue

* attempt to resolve memory running out issue

* attempt to resolve memory running out issue

* attempt to resolve memory running out issue

* attempt to resolve memory running out issue

* attempt to resolve memory running out issue

* setting wireguard

* solved memory issues

* rename landing page template

* modify wireguard enabled fn

* layout change

* syntax fix

* modify node setup script

* sync up envs

* return missing function

* fix urls

* fix network manager script execution

* fix wss and nginx

* fix layout

* tweak WG contion

* syntax fix

* add init placeholder

* syntax fix

* redefine wireguard check logic

* check if node exists

* add argparse and dev option

* styling

* add panic

* add error message

* improve logic

* improve logic

* add arg

* add dev arg for all levels

* add confirmation loop

* styling

* fix bonding question

* syntax edit

* syntax edit

* syntax edit

* refactor for already bonded nodes

* add default branch on top and define metavar

* fix node install script

* clean and prepare for review

* indentation fix

* fix nginx setup

* fix nginx setup

* style cleanup

* fix try error logic

* tune --dev option to run before command correctly

* fix y/n convention across the modules

* add explorer URL to the message

* minor layout fixes
This commit is contained in:
import this
2025-09-02 20:34:24 +00:00
committed by GitHub
parent 3d6cf730c2
commit 43d043a9cd
8 changed files with 1421 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nym Node</title>
<style>
body {
font-family: sans-serif;
text-align: center;
padding: 2em;
background-color: #111;
color: #0ff;
}
h1 {
margin-bottom: 0.5em;
}
</style>
</head>
<body>
<h1>Nym Node</h1>
<p>This is a devrel testing placeholder page for Nym Node landing page.</p>
</body>
</html>
+637
View File
@@ -0,0 +1,637 @@
#!/usr/bin/python3
__version__ = "1.0.0"
__default_branch__ = "develop"
import os
import re
import sys
import subprocess
import argparse
import tempfile
import shlex
import time
from datetime import datetime
from pathlib import Path
from typing import Iterable, Optional, Mapping
from typing import Optional, Tuple
class NodeSetupCLI:
"""All CLI main functions"""
def __init__(self, args):
self.branch = args.dev
self.welcome_message = self.print_welcome_message()
self.mode = self.prompt_mode()
self.prereqs_install_sh = self.fetch_script("nym-node-prereqs-install.sh")
self.env_vars_install_sh = self.fetch_script("setup-env-vars.sh")
self.node_install_sh = self.fetch_script("nym-node-install.sh")
self.service_config_sh = self.fetch_script("setup-systemd-service-file.sh")
self.start_node_systemd_service_sh = self.fetch_script("start-node-systemd-service.sh")
self.landing_page_html = self._check_gwx_mode() and self.fetch_script("landing-page.html")
self.nginx_proxy_wss_sh = self._check_gwx_mode() and self.fetch_script("nginx_proxy_wss_sh")
self.tunnel_manager_sh = self._check_gwx_mode() and self.fetch_script("network_tunnel_manager.sh")
self.wg_ip_tables_manager_sh = self._check_gwx_mode() and self.fetch_script("wireguard-exit-policy-manager.sh")
self.wg_ip_tables_test_sh = self._check_gwx_mode() and self.fetch_script("exit-policy-tests.sh")
def print_welcome_message(self):
"""Welcome user, warns for needed pre-reqs and asks for confimation"""
self.print_character("=", 41)
print(\
"* * * * * * NYM - NODE - CLI * * * * * *\n" \
"An interactive tool to download, install\n" \
"* * * * * setup & run nym-node * * * * *"
)
self.print_character("=", 41)
msg = \
"Before you begin, make sure that:\n"\
"1. You run this setup on Debian based Linux (ie Ubuntu)\n"\
"2. You run this installation program from a root shell\n"\
"3. You meet minimal requirements: https://nym.com/docs/operators/nodes\n"\
"4. You accept Operators Terms & Conditions: https://nym.com/operators-validators-terms\n"\
"5. You have Nym wallet with at least 101 NYM: https://nym.com/docs/operators/nodes/preliminary-steps/wallet-preparation\n"\
"6. In case of Gateway behind reverse proxy, you have A and AAAA DNS record pointing to this IP and propagated\n"\
"\nTo confirm and continue, write 'ACCEPT' and press enter:"
print(msg)
confirmation = input("\n")
if confirmation.upper() == "ACCEPT":
pass
else:
print("Without confirming the points above, we cannot continue.")
exit(1)
def prompt_mode(self):
"""Ask user to insert node functionality and save it in python and bash envs"""
mode = input(
"\nEnter the mode you want to run nym-node in: "
"\n1. mixnode "
"\n2. entry-gateway "
"\n3. exit-gateway (works as entry-gateway as well) "
"\nPress 1, 2 or 3 and enter:\n"
).strip()
if mode in ("1", "mixnode"):
mode = "mixnode"
elif mode in ("2", "entry-gateway"):
mode = "entry-gateway"
elif mode in ("3", "exit-gateway"):
mode = "exit-gateway"
else:
print("Only numbers 1, 2 or 3 are accepted.")
raise SystemExit(1)
# save mode for this Python instance
self.mode = mode
os.environ["MODE"] = mode
# persist to env.sh so other scripts can source it
env_file = Path("env.sh")
with env_file.open("a") as f:
f.write(f'export MODE="{mode}"\n')
# source env.sh so future bash subprocesses see it immediately
subprocess.run("source ./env.sh", shell=True, executable="/bin/bash")
print(f"Mode set to '{mode}' — stored in env.sh and sourced for immediate use.")
return mode
def fetch_script(self, script_name):
"""Fetches needed scripts according to a defined mode"""
# print header only the first time
if not getattr(self, "_fetched_once", False):
print("\n* * * Fetching required scripts * * *")
self._fetched_once = True
url = self._return_script_url(script_name)
print(f"Fetching file from: {url}")
result = subprocess.run(["wget", "-qO-", url], capture_output=True, text=True)
if result.returncode != 0 or not result.stdout.strip():
print(f"wget failed to download the file.")
print("stderr:", result.stderr)
raise RuntimeError(f"Failed to fetch {url}")
# Optional sanity check:
first_line = result.stdout.splitlines()[0] if result.stdout else ""
print(f"Downloaded {len(result.stdout)} bytes.")
return result.stdout
def _return_script_url(self, script_init_name):
"""Dictionary pointing to scripts url returning value according to a passed key"""
github_raw_nymtech_nym_scripts_url = f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/{self.branch}/scripts/"
scripts_urls = {
"nym-node-prereqs-install.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/nym-node-prereqs-install.sh",
"setup-env-vars.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-env-vars.sh",
"nym-node-install.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/nym-node-install.sh",
"setup-systemd-service-file.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-systemd-service-file.sh",
"start-node-systemd-service.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/start-node-systemd-service.sh",
"nginx_proxy_wss_sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-nginx-proxy-wss.sh",
"landing-page.html": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/landing-page.html",
"network_tunnel_manager.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/network_tunnel_manager.sh",
"wireguard-exit-policy-manager.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh",
"exit-policy-tests.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/exit-policy-tests.sh",
}
return scripts_urls[script_init_name]
def run_script(
self,
script_text: str,
args: Optional[Iterable[str]] = None,
env: Optional[Mapping[str, str]] = None,
cwd: Optional[str] = None,
sudo: bool = False, # ignored for root; kept for signature compat
detached: bool = False,
) -> int:
"""
Save script to a temp file and run it
- Automatically injects ENV_FILE=<abs path to ./env.sh> unless already provided
- Adds SYSTEMD_PAGER="" and SYSTEMD_COLORS="0" by default
Returns exit code (0 if detached fire-and-forget)
"""
import os, subprocess
path = self._write_temp_script(script_text)
try:
# build env with sensible defaults
run_env = dict(os.environ)
if env:
run_env.update(env)
# ensure ENV_FILE is absolute and present for all scripts
if "ENV_FILE" not in run_env:
# if env.sh is elsewhere, change this to your known base dir
env_file = os.path.abspath(os.path.join(os.getcwd(), "env.sh"))
run_env["ENV_FILE"] = env_file
# make systemctl non-interactive everywhere
run_env.setdefault("SYSTEMD_PAGER", "")
run_env.setdefault("SYSTEMD_COLORS", "0")
cmd = [str(path)] + (list(args) if args else [])
if detached:
subprocess.Popen(
cmd,
env=run_env,
cwd=cwd,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
close_fds=True,
)
return 0
else:
cp = subprocess.run(cmd, env=run_env, cwd=cwd)
return cp.returncode
finally:
try:
path.unlink(missing_ok=True)
except Exception:
pass
def _write_temp_script(self, script_text: str) -> Path:
"""Helper: write script text to a temp file, ensure bash shebang, chmod +x, return its path"""
if not script_text.lstrip().startswith("#!"):
script_text = "#!/usr/bin/env bash\n" + script_text
with tempfile.NamedTemporaryFile("w", delete=False, suffix=".sh") as f:
f.write(script_text)
path = Path(f.name)
os.chmod(path, 0o700)
return path
def _check_gwx_mode(self):
"""Helper: Several fns run only for GWx - this fn checks this condition"""
if self.mode == "exit-gateway":
return True
else:
return False
def check_wg_enabled(self):
"""Checks if Wireguard is enabled and if not, prompts user if they want to enable it, stores it to env.sh"""
env_file = os.path.abspath(os.path.join(os.getcwd(), "env.sh"))
def norm(v): # -> "true" or "false"
return "true" if str(v).strip().lower() in ("1", "true", "yes", "y") else "false"
# precedence: process env → env.sh → prompt
val = os.environ.get("WIREGUARD")
if val is None and os.path.isfile(env_file):
try:
with open(env_file, "r", encoding="utf-8") as f:
m = re.search(r'^\s*export\s+WIREGUARD\s*=\s*"?([^"\n]+)"?', f.read(), re.M)
if m:
val = m.group(1)
except Exception:
pass
if val is None:
ans = input(
"\nWireGuard is not configured.\n"
"Nodes routing WireGuard can be listed as both entry and exit in the app.\n"
"Enable WireGuard support? (y/n): "
).strip().lower()
val = "true" if ans in ("y", "yes") else "false"
val = norm(val)
os.environ["WIREGUARD"] = val
# persist to env.sh (replace or append)
try:
text = ""
if os.path.isfile(env_file):
with open(env_file, "r", encoding="utf-8") as f:
text = f.read()
if re.search(r'^\s*export\s+WIREGUARD\s*=.*$', text, re.M):
text = re.sub(r'^\s*export\s+WIREGUARD\s*=.*$', f'export WIREGUARD="{val}"', text, flags=re.M)
else:
if text and not text.endswith("\n"):
text += "\n"
text += f'export WIREGUARD="{val}"\n'
with open(env_file, "w", encoding="utf-8") as f:
f.write(text)
print(f'WIREGUARD={val} saved to {env_file}')
except Exception as e:
print(f"Warning: could not write {env_file}: {e}")
return (val == "true")
def run_bash_command(self, command, args=None, *, env=None, cwd=None, check=True):
"""
Run a command with optional args (no script stdin)
`command` can be a string (e.g., "ls") or a list (e.g., ["ls", "-la"]).
"""
# normalize command into a list
if isinstance(command, str):
cmd = shlex.split(command)
else:
cmd = list(command)
if args:
cmd += list(args)
print("Running:", " ".join(shlex.quote(c) for c in cmd))
return subprocess.run(cmd, env=env, cwd=cwd, check=check)
def run_tunnel_manager_setup(self):
"""A standalone fn to pass full cmd list needed for correct setup and test network tunneling, using an external script"""
print(
"\n* * * Setting up network configuration for mixnet IP router and Wireguard tunneling * * *"
"\nMore info: https://nym.com/docs/operators/nodes/nym-node/configuration#1-download-network_tunnel_managersh-make-executable-and-run"
"\nThis may take a while; follow the steps below and don't kill the process..."
)
# each entry is the exact argv to pass to the script
steps = [
["check_nymtun_iptables"],
["remove_duplicate_rules", "nymtun0"],
["remove_duplicate_rules", "nymwg"],
["check_nymtun_iptables"],
["adjust_ip_forwarding"],
["apply_iptables_rules"],
["check_nymtun_iptables"],
["apply_iptables_rules_wg"],
["configure_dns_and_icmp_wg"],
["adjust_ip_forwarding"],
["check_ipv6_ipv4_forwarding"],
["joke_through_the_mixnet"],
["joke_through_wg_tunnel"],
]
for argv in steps:
print("Running: network_tunnel_manager.sh", *argv)
rc = self.run_script(self.tunnel_manager_sh, args=argv)
if rc != 0:
print(f"Step {' '.join(argv)} failed with exit code {rc}. Stopping.")
return rc
print("Network tunnel manager setup completed successfully.")
return 0
def setup_test_wg_ip_tables(self):
"""Configuration and test of Wireguard exit policy according to mixnet exit policy using external scripts"""
print(
"Setting up Wireguard IP tables to match Nym exit policy for mixnet, stored at: https://nymtech.net/.wellknown/network-requester/exit-policy.txt"
"\nThis may take a while, follow the steps below and don't kill the process..."
)
self.run_script(self.wg_ip_tables_manager_sh, args=["install"])
self.run_script(self.wg_ip_tables_manager_sh, args=["status"])
self.run_script(self.wg_ip_tables_test_sh)
def run_nym_node_as_service(self):
"""Starts /etc/systemd/system/nym-node.service based on prompt using external script"""
service = "nym-node.service"
service_path = "/etc/systemd/system/nym-node.service"
print(f"\n* * * We are going to start {service} from systemd config located at: {service_path} * * *")
# if the service file is missing, run setup non-interactively
if not os.path.isfile(service_path):
print(f"Service file not found at {service_path}. Running setup...")
setup_env = {
**os.environ,
"SYSTEMD_PAGER": "",
"SYSTEMD_COLORS": "0",
"NONINTERACTIVE": "1",
"MODE": os.environ.get("MODE", "mixnode"),
}
self.run_script(self.service_config_sh, env=setup_env)
if not os.path.isfile(service_path):
print("Service file still not found after setup. Aborting.")
return
run_env = {**os.environ, "SYSTEMD_PAGER": "", "SYSTEMD_COLORS": "0", "WAIT_TIMEOUT": "600"}
is_active = subprocess.run(["systemctl", "is-active", "--quiet", service], env=run_env).returncode == 0
if is_active:
while True:
ans = input(f"{service} is already running. Restart it now? (y/n):\n").strip().lower()
if ans == "y":
self.run_script(self.start_node_systemd_service_sh, args=["restart-poll"], env=run_env)
return
elif ans == "n":
print("Continuing without restart.")
return
else:
print("Invalid input. Please press 'y' or 'n' and press enter.")
else:
while True:
ans = input(f"{service} is not running. Start it now? (y/n):\n").strip().lower()
if ans == "y":
self.run_script(self.start_node_systemd_service_sh, args=["start-poll"], env=run_env)
return
elif ans == "n":
print("Okay, not starting it.")
return
else:
print("Invalid input. Please press 'y' or 'n' and press enter.")
def run_bonding_prompt(self):
"""Interactive function navigating user to bond node"""
print("\n")
print("* * * Bonding Nym Node * * *")
print("Time to register your node to Nym Network by bonding it using Nym wallet ...")
node_path = os.path.expandvars(os.path.expanduser("$HOME/nym-binaries/nym-node"))
if not (os.path.isfile(node_path) and os.access(node_path, os.X_OK)):
print(f"Nym node not found at {node_path}, we cannot run a bonding prompt!")
exit(1)
else:
while True:
subprocess.run([os.path.expanduser(node_path), "bonding-information"])
self.run_bash_command(command="curl", args=["-4", "https://ifconfig.me"])
print("\n")
self.print_character("=", 56)
print("* * * FOLLOW THESE STEPS TO BOND YOUR NODE * * *")
print("If you already bonded your node before, just press enter")
self.print_character("=", 56)
print(
"1. Open your wallet and go to Bonding menu\n"
"2. Paste Identity key and your IP address (printed above)\n"
"3. Setup your operators cost and profit margin\n"
"4. Copy the long contract message from your wallet"
)
msg = "5. Paste the contract message from clipboard here and press enter:\n"
contract_msg = input(msg).strip()
if contract_msg == "":
print("Skipping bonding process as your node is already bonded\n")
return
else:
subprocess.run([
os.path.expanduser(node_path),
"sign",
"--contract-msg",
contract_msg
])
print(
"6. Copy the last part of the string back to your Nym wallet\n"
"7. Confirm the transaction"
)
confirmation = input(
"\n* * * Is your node bonded?\n"
"1. YES\n"
"2. NO, try again\n"
"3. Skip bonding for now\n"
"Press 1, 2, or 3 and enter:\n"
).strip()
if confirmation == "1":
# NEW: fetch identity + composed message and print it
_, message = self._explorer_message_from_identity(node_path)
self.print_character("*", 42)
print(message)
self.print_character("*", 42)
return
elif confirmation == "3":
print(
"Your node is not bonded, we are skipping this step.\n"
"Note that without bonding network tunnel manager will not work fully!\n"
"You can always bond manually using:\n"
"`$HOME/nym-binaries/nym-node sign --contract-msg <CONTRACT_MESSAGE>`"
)
return
elif confirmation == "2":
continue
else:
print(
"Your input was wrong, we are skipping this step. You can always bond manually using:\n"
"`$HOME/nym-binaries/nym-node sign --contract-msg <CONTRACT_MESSAGE>`"
)
return
def _explorer_message_from_identity(self, node_path: str) -> Tuple[Optional[str], str]:
"""
Runs `$HOME/nym-binaries/nym-node bonding-information` to
extract the id_key and returns explorer URL with a message
else return the message without the URL
"""
try:
cp = subprocess.run(
[os.path.expanduser(node_path), "bonding-information"],
capture_output=True, text=True, check=False, timeout=30
)
output = cp.stdout or ""
except Exception as e:
output = ""
# still return the generic message
key = None
msg = (
"* * * C O N G R A T U L A T I O N ! * * *\n"
"Your Nym node is registered to Nym network\n"
"Wait until the end of epoch for the change\n"
"to propagate (max 60 min)\n"
"(Could not obtain Identity Key automatically.)"
)
return key, msg
# parse the id_key
m = re.search(r"^Identity Key:\s*([A-Za-z0-9]+)\s*$", output, flags=re.MULTILINE)
key = m.group(1) if m else None
base_msg = (
"* * * C O N G R A T U L A T I O N ! * * *\n"
"Your Nym node is registered to Nym network\n"
"Wait until the end of epoch for the change\n"
"to propagate (max 60 min)\n"
)
if key:
url = f"https://explorer.nym.spectredao.net/nodes/{key}"
msg = base_msg + f"Then you can see your node at:\n{url}"
else:
msg = base_msg + "(Could not obtain Identity Key automatically.)"
return key, msg
def print_character(self, ch: str, count: int):
"""Print `ch` repeated `count` times (no unbounded growth)"""
if not ch:
return
# Use exactly one codepoint char; trim if longer
ch = ch[:1]
# Clamp count to a sensible max to avoid huge outputs
try:
n = int(count)
except Exception:
n = 0
n = max(0, min(n, 161))
print(ch * n)
def _env_with_envfile(self) -> dict:
"""Helper for env persistence sanity"""
env = dict(os.environ)
env["SYSTEMD_PAGER"] = ""
env["SYSTEMD_COLORS"] = "0"
env["ENV_FILE"] = os.path.abspath(os.path.join(os.getcwd(), "env.sh"))
return env
def run_node_installation(self,args):
"""Main function called by argparser command install running full node install flow"""
self.run_script(self.prereqs_install_sh)
self.run_script(self.env_vars_install_sh)
self.run_script(self.node_install_sh)
self.run_script(self.service_config_sh)
self._check_gwx_mode() and self.run_script(self.nginx_proxy_wss_sh)
self.run_nym_node_as_service()
self.run_bonding_prompt()
if self._check_gwx_mode():
self.run_tunnel_manager_setup()
if self.check_wg_enabled():
self.setup_test_wg_ip_tables()
self.setup_test_wg_ip_tables()
class ArgParser:
"""CLI argument interface managing the NodeSetupCLI functions based on user input"""
def parser_main(self):
# shared options to work before and after subcommands
parent = argparse.ArgumentParser(add_help=False)
parent.add_argument(
"-V", "--version",
action="version",
version=f"nym-node-cli {__version__}"
)
parent.add_argument("-d", "--dev", metavar="BRANCH",
help="Define github branch",
type=str,
default=argparse.SUPPRESS)
parent.add_argument("-v", "--verbose", action="store_true",
help="Show full error tracebacks")
parser = argparse.ArgumentParser(
prog="nym-node-cli",
description="An interactive tool to download, install, setup and run nym-node",
epilog="Privacy infrastructure operated by people around the world",
parents=[parent],
)
subparsers = parser.add_subparsers(dest="command", help="subcommands")
subparsers.required = True
p_install = subparsers.add_parser(
"install", parents=[parent],
help="Starts nym-node installation setup CLI",
aliases=["i", "I"], add_help=True
)
args = parser.parse_args()
# assign default manually only if user didnt supply --dev
if not hasattr(args, "dev"):
args.dev = __default_branch__
try:
# build CLI with parsed args to catch errors soon
cli = NodeSetupCLI(args)
commands = {
"install": cli.run_node_installation,
"i": cli.run_node_installation,
"I": cli.run_node_installation,
}
func = commands.get(args.command)
if func is None:
parser.print_help()
parser.error(f"Unknown command: {args.command}")
# execute subcommand within error test
func(args)
except SystemExit:
raise
except RuntimeError as e:
print(f"{e}\nMake sure that the your BRANCH ('{args.dev}') provided in --dev option contains this program.")
sys.exit(1)
except Exception as e:
if getattr(args, "verbose", False):
traceback.print_exc()
else:
print(f"error: {e}", file=sys.stderr)
sys.exit(1)
class SystemSafeGuards:
"""A few safe guards to deal with memory usage by this program"""
def _protect_from_oom(self, score: int = -900):
try:
with open("/proc/self/oom_score_adj", "w") as f:
f.write(str(score))
except Exception:
pass
def _trim_memory(self):
"""Liberate freeable Python objects and return arenas to the OS if possible"""
try:
import gc, ctypes
gc.collect()
try:
libc = ctypes.CDLL("libc.so.6")
# 0 = “trim as much as possible”
libc.malloc_trim(0)
except Exception:
pass
except Exception:
pass
def _cap_controller_memory(self, bytes_limit: int = 2 * 1024**3):
# limit this Python process to e.g. 2 GiB virtual memory
try:
import resource
resource.setrlimit(resource.RLIMIT_AS, (bytes_limit, bytes_limit))
except Exception:
pass
if __name__ == '__main__':
safeguards = SystemSafeGuards()
safeguards._protect_from_oom(-900) # de-prioritize controller as OOM victim
safeguards._cap_controller_memory(2 * 1024**3) # optional: cap controller to 2 GiB
app = ArgParser()
app.parser_main()
+227
View File
@@ -0,0 +1,227 @@
#!/bin/bash
set -euo pipefail
echo -e "\n* * * Ensuring ~/nym-binaries exists * * *"
mkdir -p "$HOME/nym-binaries"
# Load env.sh via absolute path if provided, else try ./env.sh
if [[ -n "${ENV_FILE:-}" && -f "${ENV_FILE}" ]]; then
set -a
# shellcheck disable=SC1090
. "${ENV_FILE}"
set +a
elif [[ -f "./env.sh" ]]; then
set -a
# shellcheck disable=SC1091
. ./env.sh
set +a
fi
# check for existing node config and optionally reset
NODE_CONFIG_DIR="$HOME/.nym/nym-nodes/default-nym-node"
check_existing_config() {
# proceed only if dir exists AND has any entries inside
if [[ -d "$NODE_CONFIG_DIR" ]] && find "$NODE_CONFIG_DIR" -mindepth 1 -maxdepth 1 | read -r _; then
echo
echo "Nym node configuration already exist at $NODE_CONFIG_DIR"
echo
echo "Initialising nym-node again will NOT overwrite your existing private keys, only adjust your preferences (like mode, wireguard optionality etc)."
echo
echo "If you want to remove your current node configuration and all data files including nodes keys type 'RESET' and press enter."
echo
read -r -p "To keep your existing node and just change its preferences press enter: " resp
if [[ "${resp}" =~ ^([Rr][Ee][Ss][Ee][Tt])$ ]]; then
echo
read -r -p "We are going to remove the existing node with configuration $NODE_CONFIG_DIR and replace it with a fresh one, do you want to back up the old one first? (y/n) " backup_ans
if [[ "${backup_ans}" =~ ^[Yy]$ ]]; then
ts="$(date +%Y%m%d-%H%M%S)"
backup_dir="$HOME/.nym/backup/$(basename "$NODE_CONFIG_DIR")-$ts"
echo "Backing up to: $backup_dir"
mkdir -p "$(dirname "$backup_dir")"
cp -a "$NODE_CONFIG_DIR" "$backup_dir"
fi
echo "Removing $NODE_CONFIG_DIR ..."
rm -rf "$NODE_CONFIG_DIR"
echo "Old node removed. Proceeding with fresh initialization..."
else
echo "Keeping existing node configuration. Proceeding to re-configure."
export ASK_WG="1"
fi
fi
}
# run the check before any initialization
check_existing_config
echo -e "\n* * * Resolving latest release tag URL * * *"
LATEST_TAG_URL="$(curl -sI -L -o /dev/null -w '%{url_effective}' https://github.com/nymtech/nym/releases/latest)"
# expected example: https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.13-emmental
if [[ -z "${LATEST_TAG_URL}" || "${LATEST_TAG_URL}" != *"/releases/tag/"* ]]; then
echo "ERROR: Could not resolve latest tag URL from GitHub." >&2
exit 1
fi
DOWNLOAD_URL="${LATEST_TAG_URL/tag/download}/nym-node"
NYM_NODE="$HOME/nym-binaries/nym-node"
# if binary already exists, ask to overwrite; if yes, remove first
if [[ -e "${NYM_NODE}" ]]; then
echo
echo -e "\n* * * A nym-node binary already exists at: ${NYM_NODE}"
read -r -p "Overwrite with the latest release? (y/n): " ow_ans
if [[ "${ow_ans}" =~ ^[Yy]$ ]]; then
echo "Removing existing binary to avoid 'text file busy'..."
rm -f "${NYM_NODE}"
else
echo "Keeping existing binary."
fi
fi
echo -e "\n* * * Downloading nym-node from:"
echo " ${DOWNLOAD_URL}"
# only download if file is missing (or we just removed it)
if [[ ! -e "${NYM_NODE}" ]]; then
curl -fL "${DOWNLOAD_URL}" -o "${NYM_NODE}"
fi
echo -e "\n * * * Making binary executable * * *"
chmod +x "${NYM_NODE}"
echo "---------------------------------------------------"
echo "Nym node binary downloaded:"
"${NYM_NODE}" --version || true
echo "---------------------------------------------------"
# check that MODE is set (after sourcing env.sh)
if [[ -z "${MODE:-}" ]]; then
echo "ERROR: Environment variable MODE is not set."
echo "Please export MODE as one of: mixnode, entry-gateway, exit-gateway"
exit 1
fi
# determine public IP (fallback if ifconfig.me fails)
echo -e "\n* * * Discovering public IP (IPv4) * * *"
if ! PUBLIC_IP="$(curl -fsS -4 https://ifconfig.me)"; then
PUBLIC_IP="$(curl -fsS https://api.ipify.org || echo '')"
fi
if [[ -z "${PUBLIC_IP}" ]]; then
echo "WARNING: Could not determine public IP automatically."
fi
# respect existing WIREGUARD; for gateways: prompt if unset OR if we kept config and ASK_WG=1
WIREGUARD="${WIREGUARD:-}"
if [[ ( "$MODE" == "entry-gateway" || "$MODE" == "exit-gateway" ) && ( -n "${ASK_WG:-}" || -z "$WIREGUARD" ) ]]; then
echo
echo "Gateways can also route WireGuard in NymVPN."
echo "Enabling it means your node may be listed as both entry and exit in the app."
# show current default in the prompt if present
def_hint=""
[[ -n "${WIREGUARD}" ]] && def_hint=" [current: ${WIREGUARD}]"
read -r -p "Enable WireGuard support? (y/n)${def_hint}: " answer || true
case "${answer:-}" in
[Yy]* ) WIREGUARD="true" ;;
[Nn]* ) WIREGUARD="false" ;;
* ) : ;; # keep existing value if user just pressed enter
esac
fi
# final default only if still empty
WIREGUARD="${WIREGUARD:-false}"
# persist WIREGUARD to the same env file Python CLI uses
ENV_PATH="${ENV_FILE:-./env.sh}"
if [[ -n "$ENV_PATH" ]]; then
mkdir -p "$(dirname "$ENV_PATH")"
if [[ -f "$ENV_PATH" ]]; then
# replace existing export or append
if grep -qE '^[[:space:]]*export[[:space:]]+WIREGUARD=' "$ENV_PATH"; then
sed -i -E 's|^[[:space:]]*export[[:space:]]+WIREGUARD=.*$|export WIREGUARD="'"$WIREGUARD"'"|' "$ENV_PATH"
else
printf '\nexport WIREGUARD="%s"\n' "$WIREGUARD" >> "$ENV_PATH"
fi
else
printf 'export WIREGUARD="%s"\n' "$WIREGUARD" > "$ENV_PATH"
fi
echo "WIREGUARD=${WIREGUARD} persisted to $ENV_PATH"
fi
# helpers: ensure optional env vars exist (avoid -u issues)
HOSTNAME="${HOSTNAME:-}"
LOCATION="${LOCATION:-}"
EMAIL="${EMAIL:-}"
MONIKER="${MONIKER:-}"
DESCRIPTION="${DESCRIPTION:-}"
# initialize node config
case "${MODE}" in
mixnode)
echo -e "\n* * * Initialising nym-node in mode: mixnode * * *"
"${NYM_NODE}" run \
--mode mixnode \
${PUBLIC_IP:+--public-ips "$PUBLIC_IP"} \
${HOSTNAME:+--hostname "$HOSTNAME"} \
${LOCATION:+--location "$LOCATION"} \
-w \
--init-only
;;
entry-gateway)
echo -e "\n* * * Initialising nym-node in mode: entry-gateway * * *"
"${NYM_NODE}" run \
--mode entry-gateway \
${PUBLIC_IP:+--public-ips "$PUBLIC_IP"} \
${HOSTNAME:+--hostname "$HOSTNAME"} \
${LOCATION:+--location "$LOCATION"} \
--wireguard-enabled "${WIREGUARD}" \
${HOSTNAME:+--landing-page-assets-path "/var/www/${HOSTNAME}"} \
-w \
--init-only
;;
exit-gateway)
echo -e "\n* * * Initialising nym-node in mode: exit-gateway * * *"
if [[ -z "${HOSTNAME:-}" || -z "${LOCATION:-}" ]]; then
echo "ERROR: HOSTNAME and LOCATION must be exported for exit-gateway."
exit 1
fi
"${NYM_NODE}" run \
--mode exit-gateway \
${PUBLIC_IP:+--public-ips "$PUBLIC_IP"} \
--hostname "$HOSTNAME" \
--location "$LOCATION" \
--wireguard-enabled "${WIREGUARD:-false}" \
--announce-wss-port 9001 \
--landing-page-assets-path "/var/www/${HOSTNAME}" \
-w \
--init-only
;;
*)
echo "ERROR: Unsupported MODE: '${MODE}'"
echo "Valid values: mixnode, entry-gateway, exit-gateway"
exit 1
;;
esac
echo
echo "* * * nym-node initialised. Config path should be:"
echo " $HOME/.nym/nym-nodes/default-nym-node/"
# setup description.toml (if init created the dir)
DESC_DIR="$HOME/.nym/nym-nodes/default-nym-node/data"
DESC_FILE="$DESC_DIR/description.toml"
if [[ -d "$DESC_DIR" ]]; then
echo -e "\n* * * Writing node description: $DESC_FILE * * *"
mkdir -p "$DESC_DIR"
cat > "$DESC_FILE" <<EOF
moniker = "${MONIKER}"
website = "${HOSTNAME}"
security_contact = "${EMAIL}"
details = "${DESCRIPTION}"
EOF
echo "* * * Node description saved * * *"
echo "You can edit it later at: $DESC_FILE (restart node to apply)."
else
echo "NOTE: Description directory not found yet ($DESC_DIR)."
echo " It will exist after a full init; you can create the file later."
fi
@@ -0,0 +1,29 @@
#!/bin/bash
# update, upgrade & install dependencies
echo -e "\n* * * Installing needed prerequisities * * *"
apt update -y && apt --fix-broken install
apt upgrade
apt install apt ca-certificates jq curl wget ufw jq tmux pkg-config build-essential libssl-dev git ntp ntpdate neovim tree tmux tig nginx -y
apt install ufw --fix-missing
# enable & setup firewall
echo -e "\n* * * Setting up firewall using ufw * * * "
echo "Please enable the firewall in the next prompt for node proper routing!"
echo
ufw enable
ufw allow 22/tcp # SSH - you're in control of these ports
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # HTTPS
ufw allow 1789/tcp # Nym specific
ufw allow 1790/tcp # Nym specific
ufw allow 8080/tcp # Nym specific - nym-node-api
ufw allow 9000/tcp # Nym Specific - clients port
ufw allow 9001/tcp # Nym specific - wss port
ufw allow 51822/udp # WireGuard
ufw allow 'Nginx Full' && \
ufw reload && \
ufw status
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# setup_env.sh
# set -euo pipefail
echo -e "\n* * * Setting up environmental variables to ./env.sh * * *"
# detect if we're being sourced
if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then
__SOURCED=1
else
__SOURCED=0
fi
while true; do
# prompt user
read -rp "Enter hostname (if you don't use a DNS, press enter): " HOSTNAME
read -rp "Enter node location (country code or name): " LOCATION
read -rp "Enter your email: " EMAIL
read -rp "Enter node public moniker (visible in the explorer and NymVPN app): " MONIKER
read -rp "Enter node public description: " DESCRIPTION
# show summary table
echo -e "\nPlease confirm the values you entered:"
echo "---------------------------------------"
printf "%-20s %s\n" "HOSTNAME:" "$HOSTNAME"
printf "%-20s %s\n" "LOCATION:" "$LOCATION"
printf "%-20s %s\n" "EMAIL:" "$EMAIL"
printf "%-20s %s\n" "MONIKER:" "$MONIKER"
printf "%-20s %s\n" "DESCRIPTION:" "$DESCRIPTION"
echo "---------------------------------------"
read -rp "Are these correct? (y/n): " CONFIRM
case "$CONFIRM" in
[Yy]* ) break ;; # confirmed, exit loop
[Nn]* ) echo -e "\nLet's try again...\n" ;; # loop restarts
* ) echo "Please answer y or n." ;;
esac
done
# try to get the latest binary URL (non-fatal if missing)
LATEST_BINARY=$(
curl -fsSL https://github.com/nymtech/nym/releases/latest \
| grep -Eo 'href="/nymtech/nym/releases/download/[^"]+/nym-node"' \
| head -n1 \
| cut -d'"' -f2
)
if [[ -z "${LATEST_BINARY:-}" ]]; then
echo "WARNING: Could not determine latest nym-node binary URL right now. The installer will resolve it later."
fi
PUBLIC_IP=$(curl -fsS -4 https://ifconfig.me || true)
PUBLIC_IP=${PUBLIC_IP:-""}
# write env.sh
{
[[ -n "${LATEST_BINARY:-}" ]] && echo "export LATEST_BINARY=\"https://github.com${LATEST_BINARY}\""
echo "export HOSTNAME=\"${HOSTNAME}\""
echo "export LOCATION=\"${LOCATION}\""
echo "export EMAIL=\"${EMAIL}\""
echo "export MONIKER=\"${MONIKER}\""
echo "export DESCRIPTION=\"${DESCRIPTION}\""
echo "export PUBLIC_IP=\"${PUBLIC_IP}\""
} > env.sh
echo -e "\nVariables saved to ./env.sh"
if [[ $__SOURCED -eq 1 ]]; then
# shellcheck disable=SC1091
. ./env.sh
echo "Loaded into current shell (because you sourced this script)."
else
echo "To load them into your current shell, run: source ./env.sh"
fi
@@ -0,0 +1,283 @@
#!/usr/bin/env bash
set -euo pipefail
# load env (prefer absolute ENV_FILE injected by Python CLI; fallback to ./env.sh)
if [[ -n "${ENV_FILE:-}" && -f "${ENV_FILE}" ]]; then
set -a; . "${ENV_FILE}"; set +a
elif [[ -f "./env.sh" ]]; then
set -a; . ./env.sh; set +a
fi
: "${HOSTNAME:?HOSTNAME not set in env.sh}"
: "${EMAIL:?EMAIL not set in env.sh}"
export SYSTEMD_PAGER=""
export SYSTEMD_COLORS="0"
DEBIAN_FRONTEND=noninteractive
# sanity check
if [[ "${HOSTNAME}" == "localhost" || "${HOSTNAME}" == "127.0.0.1" ]]; then
echo "ERROR: HOSTNAME cannot be 'localhost'. Use a public FQDN." >&2
exit 1
fi
echo -e "\n* * * Starting nginx configuration for landing page, reverse proxy and WSS * * *"
# set paths & ports vars
WEBROOT="/var/www/${HOSTNAME}"
LE_ACME_DIR="/var/www/letsencrypt"
SITES_AVAIL="/etc/nginx/sites-available"
SITES_EN="/etc/nginx/sites-enabled"
BASE_HTTP="${SITES_AVAIL}/${HOSTNAME}" # :80 vhost
BASE_HTTPS="${SITES_AVAIL}/${HOSTNAME}-ssl" # :443 vhost (well write it ourselves)
WSS_AVAIL="${SITES_AVAIL}/wss-config-nym"
BACKUP_DIR="/etc/nginx/sites-backups"
NYM_PORT_HTTP="${NYM_PORT_HTTP:-8080}"
NYM_PORT_WSS="${NYM_PORT_WSS:-9000}"
WSS_LISTEN_PORT="${WSS_LISTEN_PORT:-9001}"
mkdir -p "${WEBROOT}" "${LE_ACME_DIR}" "${BACKUP_DIR}" "${SITES_AVAIL}" "${SITES_EN}"
# helpers
neat_backup() {
local file="$1"; [[ -f "$file" ]] || return 0
local sha_now; sha_now="$(sha256sum "$file" | awk '{print $1}')" || return 0
local tag; tag="$(basename "$file")"
local latest="${BACKUP_DIR}/${tag}.latest"
if [[ -f "$latest" ]]; then
local sha_prev; sha_prev="$(awk '{print $1}' "$latest")"
[[ "$sha_now" == "$sha_prev" ]] && return 0
fi
cp -a "$file" "${BACKUP_DIR}/${tag}.bak.$(date +%s)"
echo "$sha_now ${tag}" > "$latest"
ls -1t "${BACKUP_DIR}/${tag}.bak."* 2>/dev/null | tail -n +6 | xargs -r rm -f
}
ensure_enabled() {
local src="$1"; local name; name="$(basename "$src")"
ln -sf "$src" "${SITES_EN}/${name}"
}
cert_ok() {
[[ -s "/etc/letsencrypt/live/${HOSTNAME}/fullchain.pem" && -s "/etc/letsencrypt/live/${HOSTNAME}/privkey.pem" ]]
}
fetch_landing() {
local url="https://raw.githubusercontent.com/nymtech/nym/refs/heads/feature/node-setup-cli/scripts/nym-node-setup/landing-page.html"
if command -v curl >/dev/null 2>&1; then
curl -fsSL "$url" -o "${WEBROOT}/index.html" || true
else
wget -qO "${WEBROOT}/index.html" "$url" || true
fi
if [[ ! -s "${WEBROOT}/index.html" ]]; then
cat > "${WEBROOT}/index.html" <<'HTML'
<!doctype html><html><head><meta charset="utf-8"><title>Nym Node</title></head>
<body style="font-family:sans-serif;margin:2rem">
<h1>Nym node landing</h1>
<p>This is a placeholder page served by nginx.</p>
</body></html>
HTML
fi
}
reload_nginx() { nginx -t && systemctl reload nginx; }
# landing page (idempotent)
fetch_landing
echo "Landing page at ${WEBROOT}/index.html"
# disable default and stale SSL configs
[[ -L "${SITES_EN}/default" ]] && unlink "${SITES_EN}/default" || true
for f in "${SITES_EN}"/*; do
[[ -L "$f" ]] || continue
if grep -q "/etc/letsencrypt/live/localhost" "$f"; then
echo "Disabling vhost referencing localhost cert: $f"; unlink "$f"
fi
done
for f in "${SITES_EN}"/*; do
[[ -L "$f" ]] || continue
if grep -qE 'listen\s+.*443' "$f"; then
cert=$(awk '/ssl_certificate[ \t]+/ {print $2}' "$f" | tr -d ';' | head -n1)
key=$(awk '/ssl_certificate_key[ \t]+/ {print $2}' "$f" | tr -d ';' | head -n1)
if [[ -n "${cert:-}" && ! -s "$cert" ]] || [[ -n "${key:-}" && ! -s "$key" ]]; then
echo "Disabling SSL vhost with missing cert/key: $f"; unlink "$f"
fi
fi
done
# HTTP :80 vhost (ACME-safe, proxy to :8080)
neat_backup "${BASE_HTTP}"
cat > "${BASE_HTTP}" <<EOF
server {
listen 80;
listen [::]:80;
server_name ${HOSTNAME};
# ACME challenge path (HTTP only)
location ^~ /.well-known/acme-challenge/ {
root ${LE_ACME_DIR};
default_type "text/plain";
}
root ${WEBROOT};
index index.html;
location = /favicon.ico { return 204; access_log off; log_not_found off; }
location / {
try_files \$uri \$uri/ @app;
}
location @app {
proxy_pass http://127.0.0.1:${NYM_PORT_HTTP};
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header Host \$host;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
}
}
EOF
ensure_enabled "${BASE_HTTP}"
reload_nginx
systemctl status nginx --no-pager | sed -n '1,6p' || true
# ACME preflight (informative)
echo -e "\n* * * ACME preflight checks * * *"
if ! curl -fsSL https://acme-v02.api.letsencrypt.org/directory >/dev/null; then
echo "WARNING: Can't reach Let's Encrypt directory. We'll still keep HTTP up." >&2
fi
THIS_IP="$(curl -fsS -4 https://ifconfig.me || true)"
DNS_IP="$(getent ahostsv4 "${HOSTNAME}" 2>/dev/null | awk '{print $1; exit}')"
echo "Public IPv4: ${THIS_IP:-unknown} DNS A(${HOSTNAME}): ${DNS_IP:-unresolved}"
if [[ -n "${THIS_IP:-}" && -n "${DNS_IP:-}" && "${THIS_IP}" != "${DNS_IP}" ]]; then
echo "WARNING: DNS for ${HOSTNAME} does not match this server's public IPv4."
fi
timedatectl show -p NTPSynchronized --value 2>/dev/null | grep -qi yes || timedatectl set-ntp true || true
# install certbot if missing
if ! command -v certbot >/dev/null 2>&1; then
if command -v snap >/dev/null 2>&1; then
snap install core || true; snap refresh core || true
snap install --classic certbot; ln -sf /snap/bin/certbot /usr/bin/certbot
else
apt-get update -y >/dev/null 2>&1 || true
apt-get install -y certbot >/dev/null 2>&1 || true
fi
fi
# issue/renew via WEBROOT (no nginx auto-edit), non-fatal if it fails
STAGING_FLAG=""; [[ "${CERTBOT_STAGING:-0}" == "1" ]] && STAGING_FLAG="--staging" && echo "Using Let's Encrypt STAGING."
if ! cert_ok; then
certbot certonly --non-interactive --agree-tos -m "${EMAIL}" -d "${HOSTNAME}" \
--webroot -w "${LE_ACME_DIR}" ${STAGING_FLAG} || true
fi
# create own 443 vhost (only if certs exist)
if cert_ok; then
neat_backup "${BASE_HTTPS}"
cat > "${BASE_HTTPS}" <<EOF
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ${HOSTNAME};
ssl_certificate /etc/letsencrypt/live/${HOSTNAME}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${HOSTNAME}/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
root ${WEBROOT};
index index.html;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location = /favicon.ico { return 204; access_log off; log_not_found off; }
location / {
try_files \$uri \$uri/ @app;
}
location @app {
proxy_pass http://127.0.0.1:${NYM_PORT_HTTP};
proxy_http_version 1.1;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header Host \$host;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
}
}
EOF
ensure_enabled "${BASE_HTTPS}"
# optional: redirect HTTP->HTTPS (keeps ACME path in HTTP too via separate small server)
neat_backup "${BASE_HTTP}"
cat > "${BASE_HTTP}" <<EOF
server {
listen 80;
listen [::]:80;
server_name ${HOSTNAME};
# Keep ACME reachable over HTTP:
location ^~ /.well-known/acme-challenge/ {
root ${LE_ACME_DIR};
default_type "text/plain";
}
# Redirect the rest to HTTPS
location / {
return 301 https://\$host\$request_uri;
}
}
EOF
ensure_enabled "${BASE_HTTP}"
reload_nginx
else
echo "NOTE: Cert not present yet; HTTPS (443) will not listen. Only HTTP (80) is active."
fi
# WSS TLS :9001 (only if certs exist)
if cert_ok; then
neat_backup "${WSS_AVAIL}"
cat > "${WSS_AVAIL}" <<EOF
server {
listen ${WSS_LISTEN_PORT} ssl http2;
listen [::]:${WSS_LISTEN_PORT} ssl http2;
server_name ${HOSTNAME};
ssl_certificate /etc/letsencrypt/live/${HOSTNAME}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${HOSTNAME}/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
location = /favicon.ico { return 204; access_log off; log_not_found off; }
location / {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, HEAD' always;
add_header 'Access-Control-Allow-Headers' '*' always;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header X-Forwarded-For \$remote_addr;
proxy_pass http://127.0.0.1:${NYM_PORT_WSS};
proxy_intercept_errors on;
}
}
EOF
ensure_enabled "${WSS_AVAIL}"
reload_nginx
fi
echo -e "\nDone."
if cert_ok; then
echo "HTTP : http://${HOSTNAME}/ (redirects to HTTPS)"
echo "TLS : https://${HOSTNAME}/ (served by nginx)"
echo "WSS : wss://${HOSTNAME}:${WSS_LISTEN_PORT}/ (served by nginx)"
else
echo "Only HTTP is active (no cert yet). Re-run after DNS/ACME is ready to enable HTTPS + WSS."
fi
@@ -0,0 +1,91 @@
#!/bin/bash
# service_config_sh
set -euo pipefail
SERVICE_PATH="/etc/systemd/system/nym-node.service"
normalize_mode() {
local input="${1,,}"
case "$input" in
1|"mixnode") echo "mixnode" ;;
2|"entry-gateway"|"entrygateway"|"entry") echo "entry-gateway" ;;
3|"exit-gateway"|"exitgateway"|"exit") echo "exit-gateway" ;;
*) echo "" ;;
esac
}
ensure_mode() {
local m="${MODE:-}"
if [[ -z "$m" ]]; then
read -rp "Select mode: " m
fi
m="$(normalize_mode "$m")"
while [[ -z "$m" ]]; do
echo "Invalid mode. Allowed: mixnode, entry-gateway, exit-gateway (or 1/2/3)."
read -rp "Select mode: " m
m="$(normalize_mode "$m")"
done
MODE="$m"
}
create_service_file() {
cat > "$SERVICE_PATH" <<EOF
[Unit]
Description=Nym Node
StartLimitInterval=350
StartLimitBurst=10
[Service]
User=root
LimitNOFILE=65536
ExecStart=/root/nym-binaries/nym-node run --mode ${MODE} --accept-operator-terms-and-conditions
KillSignal=SIGINT
Restart=on-failure
RestartSec=30
# If you want memory caps, uncomment and tune:
# MemoryHigh=800M
# MemoryMax=1G
# MemorySwapMax=1G
# OOMScoreAdjust=500
[Install]
WantedBy=multi-user.target
EOF
echo "Service file saved in $SERVICE_PATH, printing it below for control:"
cat "$SERVICE_PATH"
echo "* * * Reloading systemd and enabling service..."
systemctl daemon-reload
systemctl enable nym-node.service
}
echo -e "\n* * * Setting up systemd service config file for node automation * * *"
# if already exists, just reload + enable and exit
if [[ -f "$SERVICE_PATH" ]]; then
echo "Service file already exists at: $SERVICE_PATH"
echo "* * * Reloading systemd and enabling service..."
systemctl daemon-reload
systemctl enable nym-node.service
exit 0
fi
# non-interactive creation (used by Python)
if [[ "${NONINTERACTIVE:-}" = "1" ]]; then
MODE="$(normalize_mode "${MODE:-}")"
if [[ -z "$MODE" ]]; then
echo "NONINTERACTIVE=1 requires MODE to be set to 1/2/3 or mixnode|entry-gateway|exit-gateway."
exit 2
fi
create_service_file
exit 0
fi
# interactive path (manual runs)
ensure_mode
read -rp "Service file not found. Create it now? (y/n): " create_ans
if [[ "${create_ans:-}" =~ ^[Yy]$ ]]; then
create_service_file
else
echo "Not creating the service file."
fi
@@ -0,0 +1,55 @@
#!/bin/bash
# start_node_systemd_service_sh (control script)
set -euo pipefail
SERVICE="nym-node.service"
export SYSTEMD_PAGER=""
export SYSTEMD_COLORS="0"
SYSTEMCTL="systemctl --no-ask-password --quiet"
WAIT_TIMEOUT="${WAIT_TIMEOUT:-600}" # seconds
reload_and_reset() { $SYSTEMCTL daemon-reload || true; $SYSTEMCTL reset-failed "$SERVICE" || true; }
wait_until_active_or_fail() {
local deadline=$(( $(date +%s) + WAIT_TIMEOUT ))
local last=""
while [ "$(date +%s)" -lt "$deadline" ]; do
local active sub result
active="$(systemctl show -p ActiveState --value "$SERVICE" 2>/dev/null || echo unknown)"
sub="$(systemctl show -p SubState --value "$SERVICE" 2>/dev/null || echo unknown)"
result="$(systemctl show -p Result --value "$SERVICE" 2>/dev/null || echo unknown)"
local cur="${active}/${sub}/${result}"
if [ "$cur" != "$last" ]; then
echo "state: ActiveState=${active} SubState=${sub} Result=${result}"
last="$cur"
fi
[ "$active" = "active" ] && return 0
if [ "$active" = "failed" ] || [ "$result" = "failed" ] || [ "$result" = "exit-code" ] || [ "$result" = "timeout" ]; then
return 1
fi
sleep 1
done
echo "timeout: ${WAIT_TIMEOUT}s exceeded while waiting for ${SERVICE}"
return 1
}
restart_poll() {
reload_and_reset
echo "Restarting $SERVICE (non-blocking) and polling up to ${WAIT_TIMEOUT}s..."
systemctl --no-ask-password restart --no-block "$SERVICE"
wait_until_active_or_fail
}
start_poll() {
reload_and_reset
echo "Starting $SERVICE (non-blocking) and polling up to ${WAIT_TIMEOUT}s..."
systemctl --no-ask-password start --no-block "$SERVICE"
wait_until_active_or_fail
}
case "${1:-}" in
restart-poll) restart_poll ;;
start-poll) start_poll ;;
*) echo "Usage: $0 {start-poll|restart-poll}"; exit 2 ;;
esac