feat(core): Implement external node management

Introduces a dedicated system for managing a list of external nodes, each with a unique name and IP address. This feature is designed for multi-node deployments.
This commit is contained in:
Whispering Wind
2025-08-04 13:49:45 +02:00
committed by GitHub
parent 80f4f62b85
commit c5f1b6d447
3 changed files with 151 additions and 4 deletions

View File

@ -298,6 +298,41 @@ def ip_address(edit: bool, ipv4: str, ipv6: str):
click.echo(f'{e}', err=True)
@cli.group()
def node():
"""Manage external node IPs for multi-server setups."""
pass
@node.command('add')
@click.option('--name', required=True, type=str, help='A unique name for the node (e.g., "Node-DE").')
@click.option('--ip', required=True, type=str, help='The public IP address of the node.')
def add_node(name, ip):
"""Add a new external node."""
try:
output = cli_api.add_node(name, ip)
click.echo(output.strip())
except Exception as e:
click.echo(f'{e}', err=True)
@node.command('delete')
@click.option('--name', required=True, type=str, help='The name of the node to delete.')
def delete_node(name):
"""Delete an external node by its name."""
try:
output = cli_api.delete_node(name)
click.echo(output.strip())
except Exception as e:
click.echo(f'{e}', err=True)
@node.command('list')
def list_nodes():
"""List all configured external nodes."""
try:
output = cli_api.list_nodes()
click.echo(output.strip())
except Exception as e:
click.echo(f'{e}', err=True)
@cli.command('update-geo')
@click.option('--country', '-c',
type=click.Choice(['iran', 'china', 'russia'], case_sensitive=False),
@ -323,9 +358,6 @@ def masquerade(remove: bool, enable: str):
raise click.UsageError('Error: You cannot use both --remove and --enable at the same time')
if enable:
# NOT SURE THIS IS NEEDED
# if not enable.startswith('http://') and not enable.startswith('https://'):
# enable = 'https://' + enable
cli_api.enable_hysteria2_masquerade(enable)
click.echo('Masquerade enabled successfully.')
elif remove:
@ -651,4 +683,4 @@ def config_ip_limit(block_duration: int, max_ips: int):
if __name__ == '__main__':
cli()
cli()

View File

@ -32,6 +32,7 @@ class Command(Enum):
SHOW_USER_URI = os.path.join(SCRIPT_DIR, 'hysteria2', 'show_user_uri.py')
WRAPPER_URI = os.path.join(SCRIPT_DIR, 'hysteria2', 'wrapper_uri.py')
IP_ADD = os.path.join(SCRIPT_DIR, 'hysteria2', 'ip.py')
NODE_MANAGER = os.path.join(SCRIPT_DIR, 'hysteria2', 'node.py')
MANAGE_OBFS = os.path.join(SCRIPT_DIR, 'hysteria2', 'manage_obfs.py')
MASQUERADE_SCRIPT = os.path.join(SCRIPT_DIR, 'hysteria2', 'masquerade.py')
TRAFFIC_STATUS = 'traffic.py' # won't be called directly (it's a python module)
@ -426,6 +427,23 @@ def edit_ip_address(ipv4: str, ipv6: str):
if ipv6:
run_cmd(['python3', Command.IP_ADD.value, 'edit', '-6', ipv6])
def add_node(name: str, ip: str):
"""
Adds a new external node.
"""
return run_cmd(['python3', Command.NODE_MANAGER.value, 'add', '--name', name, '--ip', ip])
def delete_node(name: str):
"""
Deletes an external node by name.
"""
return run_cmd(['python3', Command.NODE_MANAGER.value, 'delete', '--name', name])
def list_nodes():
"""
Lists all configured external nodes.
"""
return run_cmd(['python3', Command.NODE_MANAGER.value, 'list'])
def update_geo(country: str):
'''

View File

@ -0,0 +1,97 @@
#!/usr/bin/env python3
import sys
import json
import argparse
from pathlib import Path
core_scripts_dir = Path(__file__).resolve().parents[1]
if str(core_scripts_dir) not in sys.path:
sys.path.append(str(core_scripts_dir))
from paths import NODES_JSON_PATH
def read_nodes():
if not NODES_JSON_PATH.exists():
return []
try:
with NODES_JSON_PATH.open("r") as f:
content = f.read()
if not content:
return []
return json.loads(content)
except json.JSONDecodeError:
sys.exit(f"Error: Could not decode JSON from {NODES_JSON_PATH}")
except (IOError, OSError) as e:
sys.exit(f"Error reading from {NODES_JSON_PATH}: {e}")
def write_nodes(nodes):
try:
NODES_JSON_PATH.parent.mkdir(parents=True, exist_ok=True)
with NODES_JSON_PATH.open("w") as f:
json.dump(nodes, f, indent=4)
except (IOError, OSError) as e:
sys.exit(f"Error writing to {NODES_JSON_PATH}: {e}")
def add_node(name: str, ip: str):
nodes = read_nodes()
if any(node['name'] == name for node in nodes):
print(f"Error: A node with the name '{name}' already exists.", file=sys.stderr)
sys.exit(1)
if any(node['ip'] == ip for node in nodes):
print(f"Error: A node with the IP '{ip}' already exists.", file=sys.stderr)
sys.exit(1)
nodes.append({"name": name, "ip": ip})
write_nodes(nodes)
print(f"Successfully added node '{name}' with IP '{ip}'.")
def delete_node(name: str):
nodes = read_nodes()
original_count = len(nodes)
nodes = [node for node in nodes if node['name'] != name]
if len(nodes) == original_count:
print(f"Error: No node with the name '{name}' found.", file=sys.stderr)
sys.exit(1)
write_nodes(nodes)
print(f"Successfully deleted node '{name}'.")
def list_nodes():
nodes = read_nodes()
if not nodes:
print("No nodes configured.")
return
print(f"{'Name':<20} {'IP Address'}")
print(f"{'-'*20} {'-'*15}")
for node in sorted(nodes, key=lambda x: x['name']):
print(f"{node['name']:<20} {node['ip']}")
def main():
parser = argparse.ArgumentParser(description="Manage external node configurations.")
subparsers = parser.add_subparsers(dest='command', required=True)
add_parser = subparsers.add_parser('add', help='Add a new node.')
add_parser.add_argument('--name', type=str, required=True, help='The unique name of the node.')
add_parser.add_argument('--ip', type=str, required=True, help='The IP address of the node.')
delete_parser = subparsers.add_parser('delete', help='Delete a node by name.')
delete_parser.add_argument('--name', type=str, required=True, help='The name of the node to delete.')
subparsers.add_parser('list', help='List all configured nodes.')
args = parser.parse_args()
if args.command == 'add':
add_node(args.name, args.ip)
elif args.command == 'delete':
delete_node(args.name)
elif args.command == 'list':
list_nodes()
if __name__ == "__main__":
main()