diff --git a/core/cli.py b/core/cli.py index 320ca19..ece85fd 100644 --- a/core/cli.py +++ b/core/cli.py @@ -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() \ No newline at end of file diff --git a/core/cli_api.py b/core/cli_api.py index 28c998b..5df4125 100644 --- a/core/cli_api.py +++ b/core/cli_api.py @@ -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): ''' diff --git a/core/scripts/hysteria2/node.py b/core/scripts/hysteria2/node.py new file mode 100644 index 0000000..2c7921b --- /dev/null +++ b/core/scripts/hysteria2/node.py @@ -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() \ No newline at end of file