Merge pull request #118 from ReturnFI/beta

Added IPLimit service
This commit is contained in:
Whispering Wind
2025-03-24 20:02:12 +03:30
committed by GitHub
14 changed files with 752 additions and 86 deletions

View File

@ -1 +1,3 @@
🚀 feat: allow updating IPv4, IPv6, or domain in IP Address Manager 🚀 feat: Add Hysteria IP limiter
🛠️ Fix: Correct Version Check Response for Up-to-Date Panel
🛠️ Fix: CONFIG_ENV conflicts by ensuring clean updates for IP4 and IP6

View File

@ -510,6 +510,39 @@ def check_version():
except Exception as e: except Exception as e:
click.echo(f"An unexpected error occurred: {e}", err=True) click.echo(f"An unexpected error occurred: {e}", err=True)
@cli.command('start-ip-limit')
def start_ip_limit():
"""Starts the IP limiter service."""
try:
cli_api.start_ip_limiter()
click.echo('IP Limiter service started successfully.')
except Exception as e:
click.echo(f'{e}', err=True)
@cli.command('stop-ip-limit')
def stop_ip_limit():
"""Stops the IP limiter service."""
try:
cli_api.stop_ip_limiter()
click.echo('IP Limiter service stopped successfully.')
except Exception as e:
click.echo(f'{e}', err=True)
@cli.command('config-ip-limit')
@click.option('--block-duration', '-bd', type=int, help='New block duration in seconds')
@click.option('--max-ips', '-mi', type=int, help='New maximum IPs per user')
def config_ip_limit(block_duration: int, max_ips: int):
"""Configures the IP limiter service parameters."""
try:
cli_api.config_ip_limiter(block_duration, max_ips)
click.echo('IP Limiter configuration updated successfully.')
if block_duration is not None:
click.echo(f' Block Duration: {block_duration} seconds')
if max_ips is not None:
click.echo(f' Max IPs per user: {max_ips}')
except Exception as e:
click.echo(f'{e}', err=True)
# endregion # endregion

View File

@ -48,6 +48,7 @@ class Command(Enum):
STATUS_WARP = os.path.join(SCRIPT_DIR, 'warp', 'status.sh') STATUS_WARP = os.path.join(SCRIPT_DIR, 'warp', 'status.sh')
SERVICES_STATUS = os.path.join(SCRIPT_DIR, 'services_status.sh') SERVICES_STATUS = os.path.join(SCRIPT_DIR, 'services_status.sh')
VERSION = os.path.join(SCRIPT_DIR, 'hysteria2', 'version.py') VERSION = os.path.join(SCRIPT_DIR, 'hysteria2', 'version.py')
LIMIT_SCRIPT = os.path.join(SCRIPT_DIR, 'hysteria2', 'limit.sh')
# region Custom Exceptions # region Custom Exceptions
@ -511,5 +512,31 @@ def check_version() -> str | None:
"""Checks if the current version is up-to-date and displays changelog if not.""" """Checks if the current version is up-to-date and displays changelog if not."""
return run_cmd(['python3', Command.VERSION.value, 'check-version']) return run_cmd(['python3', Command.VERSION.value, 'check-version'])
# endregion def start_ip_limiter():
'''Starts the IP limiter service.'''
run_cmd(['bash', Command.LIMIT_SCRIPT.value, 'start'])
def stop_ip_limiter():
'''Stops the IP limiter service.'''
run_cmd(['bash', Command.LIMIT_SCRIPT.value, 'stop'])
def config_ip_limiter(block_duration: int = None, max_ips: int = None):
'''Configures the IP limiter service.'''
if block_duration is not None and block_duration <= 0:
raise InvalidInputError("Block duration must be greater than 0.")
if max_ips is not None and max_ips <= 0:
raise InvalidInputError("Max IPs must be greater than 0.")
cmd_args = ['bash', Command.LIMIT_SCRIPT.value, 'config']
if block_duration is not None:
cmd_args.append(str(block_duration))
else:
cmd_args.append('')
if max_ips is not None:
cmd_args.append(str(max_ips))
else:
cmd_args.append('')
run_cmd(cmd_args)
# endregion # endregion

View File

@ -1,12 +1,19 @@
#!/bin/bash #!/bin/bash
source /etc/hysteria/core/scripts/path.sh source /etc/hysteria/core/scripts/path.sh
# ensure_config_env() { if [ ! -f "$CONFIG_ENV" ]; then
# if [ ! -f "$CONFIG_ENV" ]; then echo "CONFIG_ENV not found. Creating a new one..."
# echo ".configs.env not found. Creating it with default SNI=bts.com." touch "$CONFIG_ENV"
# echo "SNI=bts.com" > "$CONFIG_ENV" fi
# fi
# } update_config() {
local key=$1
local value=$2
sed -i "/^$key=/d" "$CONFIG_ENV" 2>/dev/null
echo "$key=$value" >> "$CONFIG_ENV"
}
add_ips() { add_ips() {
ipv4_address="" ipv4_address=""
@ -16,7 +23,7 @@ add_ips() {
for interface in $interfaces; do for interface in $interfaces; do
if ip addr show "$interface" > /dev/null 2>&1; then if ip addr show "$interface" > /dev/null 2>&1; then
ipv4=$(ip -o -4 addr show "$interface" | awk '{print $4}' | grep -vE '^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1]))' | head -n 1 | cut -d/ -f1) ipv4=$(ip -o -4 addr show "$interface" | awk '{print $4}' | grep -vE '^(127\\.|10\\.|192\\.168\\.|172\\.(1[6-9]|2[0-9]|3[0-1]))' | head -n 1 | cut -d/ -f1)
if [[ -z $ipv4_address && -n $ipv4 ]]; then if [[ -z $ipv4_address && -n $ipv4 ]]; then
ipv4_address=$ipv4 ipv4_address=$ipv4
fi fi
@ -28,13 +35,11 @@ add_ips() {
fi fi
done done
sed -i '/^IP4=/d' "$CONFIG_ENV" 2>/dev/null update_config "IP4" "${ipv4_address:-}"
sed -i '/^IP6=/d' "$CONFIG_ENV" 2>/dev/null update_config "IP6" "${ipv6_address:-}"
echo -e "\nIP4=${ipv4_address:-}" >> "$CONFIG_ENV"
echo "IP6=${ipv6_address:-}" >> "$CONFIG_ENV" echo "Updated IP4=${ipv4_address:-Not Found}"
# echo "IPs have been added to $CONFIG_ENV:" echo "Updated IP6=${ipv6_address:-Not Found}"
# echo "IP4=${ipv4_address:-Not Found}"
# echo "IP6=${ipv6_address:-Not Found}"
} }
edit_ip() { edit_ip() {
@ -42,19 +47,16 @@ edit_ip() {
local new_ip=$2 local new_ip=$2
if [[ $type == "-4" ]]; then if [[ $type == "-4" ]]; then
sed -i '/^IP4=/d' "$CONFIG_ENV" 2>/dev/null update_config "IP4" "$new_ip"
echo "IP4=$new_ip" >> "$CONFIG_ENV"
echo "IP4 has been updated to $new_ip." echo "IP4 has been updated to $new_ip."
elif [[ $type == "-6" ]]; then elif [[ $type == "-6" ]]; then
sed -i '/^IP6=/d' "$CONFIG_ENV" 2>/dev/null update_config "IP6" "$new_ip"
echo "IP6=$new_ip" >> "$CONFIG_ENV"
echo "IP6 has been updated to $new_ip." echo "IP6 has been updated to $new_ip."
else else
echo "Invalid option. Use -4 for IPv4 or -6 for IPv6." echo "Invalid option. Use -4 for IPv4 or -6 for IPv6."
fi fi
} }
# ensure_config_env
case "$1" in case "$1" in
add) add)
add_ips add_ips

View File

@ -0,0 +1,367 @@
#!/bin/bash
source /etc/hysteria/core/scripts/path.sh
# --- Configuration ---
SERVICE_NAME="hysteria-ip-limit.service"
# Load configurations from .configs.env
if [ -f "$CONFIG_ENV" ]; then
source "$CONFIG_ENV"
BLOCK_DURATION="${BLOCK_DURATION:-60}" # Default to 60 seconds if not set
MAX_IPS="${MAX_IPS:-1}" # Default to 1 IP if not set
grep -q "^BLOCK_DURATION=" "$CONFIG_ENV" || echo -e "\nBLOCK_DURATION=$BLOCK_DURATION" >> "$CONFIG_ENV"
grep -q "^MAX_IPS=" "$CONFIG_ENV" || echo "MAX_IPS=$MAX_IPS" >> "$CONFIG_ENV"
else
echo -e "BLOCK_DURATION=240\nMAX_IPS=5" > "$CONFIG_ENV"
fi
# --- Ensure files exist ---
[ ! -f "$CONNECTIONS_FILE" ] && echo "{}" > "$CONNECTIONS_FILE"
[ ! -f "$BLOCK_LIST" ] && touch "$BLOCK_LIST"
# --- Logging function ---
log_message() {
local level="$1"
local message="$2"
echo "[$(date +"%Y-%m-%d %H:%M:%S")] [$level] $message"
}
# --- Function to update the JSON file with new connection data ---
update_json() {
local username="$1"
local ip_address="$2"
if command -v jq &>/dev/null; then
temp_file=$(mktemp)
jq --arg user "$username" --arg ip "$ip_address" \
'.[$user] += [$ip] | .[$user] |= unique' "$CONNECTIONS_FILE" > "$temp_file"
mv "$temp_file" "$CONNECTIONS_FILE"
else
if grep -q "\"$username\"" "$CONNECTIONS_FILE"; then
# Add IP to existing username (if it doesn't exist)
if ! grep -q "\"$username\".*\"$ip_address\"" "$CONNECTIONS_FILE"; then
sed -i -E "s/(\"$username\":\s*\[)([^\]]*)/\1\2,\"$ip_address\"/" "$CONNECTIONS_FILE"
fi
else
# Add new username with IP
sed -i -E "s/\{(.*)\}/{\1,\"$username\":[\"$ip_address\"]}/" "$CONNECTIONS_FILE"
fi
fi
log_message "INFO" "Updated JSON: Added $ip_address for user $username"
}
# --- Function to remove an IP from the JSON when client disconnects ---
remove_ip() {
local username="$1"
local ip_address="$2"
if [ ! -f "$CONNECTIONS_FILE" ]; then
log_message "ERROR" "JSON file does not exist"
return
fi
if grep -q "\"$username\"" "$CONNECTIONS_FILE"; then
if command -v jq &>/dev/null; then
temp_file=$(mktemp)
jq --arg user "$username" --arg ip "$ip_address" \
'.[$user] = (.[$user] | map(select(. != $ip)))' "$CONNECTIONS_FILE" > "$temp_file"
mv "$temp_file" "$CONNECTIONS_FILE"
# Check if the user's IP list is now empty and remove the user if so
temp_file_check=$(mktemp)
jq --arg user "$username" 'if .[$user] | length == 0 then del(.[$user]) else . end' "$CONNECTIONS_FILE" > "$temp_file_check"
mv "$temp_file_check" "$CONNECTIONS_FILE"
else
# Basic sed replacement (not as reliable as jq)
sed -i -E "s/\"$ip_address\"(,|\])|\1\"$ip_address\"/\1/g" "$CONNECTIONS_FILE"
sed -i -E "s/,\s*\]/\]/g" "$CONNECTIONS_FILE"
sed -i -E "s/\[\s*,/\[/g" "$CONNECTIONS_FILE"
# VERY Basic check if user's IP list is empty and remove the user if so (less reliable)
if grep -q "\"$username\":\s*\[\s*\]" "$CONNECTIONS_FILE"; then
sed -i "/\"$username\":\s*\[\s*\][,\s]*/d" "$CONNECTIONS_FILE"
sed -i "s/,\s*\}$/\n}/" "$CONNECTIONS_FILE" # Remove trailing comma if it exists after user deletion
fi
fi
log_message "INFO" "Updated JSON: Removed $ip_address for user $username"
else
log_message "WARN" "User $username not found in JSON"
fi
}
# --- Block an IP using iptables and track it ---
block_ip() {
local ip_address="$1"
local username="$2"
local unblock_time=$(( $(date +%s) + BLOCK_DURATION ))
# Skip if already blocked
if iptables -C INPUT -s "$ip_address" -j DROP 2>/dev/null; then
log_message "INFO" "IP $ip_address is already blocked"
return
fi
# Add to iptables
iptables -I INPUT -s "$ip_address" -j DROP
# Add to block list with expiration time
echo "$ip_address,$username,$unblock_time" >> "$BLOCK_LIST"
log_message "WARN" "Blocked IP $ip_address for user $username for $BLOCK_DURATION seconds"
}
# --- Explicitly unblock an IP using iptables ---
unblock_ip() {
local ip_address="$1"
# Remove from iptables if exists
if iptables -C INPUT -s "$ip_address" -j DROP 2>/dev/null; then
iptables -D INPUT -s "$ip_address" -j DROP
log_message "INFO" "Unblocked IP $ip_address"
fi
# Remove from block list
sed -i "/$ip_address,/d" "$BLOCK_LIST"
}
# --- Block all IPs for a user ---
block_all_user_ips() {
local username="$1"
local ips=()
# Get all IPs for this user
if command -v jq &>/dev/null; then
readarray -t ips < <(jq -r --arg user "$username" '.[$user][]' "$CONNECTIONS_FILE" 2>/dev/null)
else
# Basic extraction without jq (less reliable)
ip_list=$(grep -oP "\"$username\":\s*\[\K[^\]]*" "$CONNECTIONS_FILE")
IFS=',' read -ra ip_entries <<< "$ip_list"
for entry in "${ip_entries[@]}"; do
# Extract IP from the JSON array entry
ip=$(echo "$entry" | grep -oP '".*"' | tr -d '"' | tr -d '[:space:]')
if [[ -n "$ip" ]]; then
ips+=("$ip")
fi
done
fi
# Block all IPs for this user
for ip in "${ips[@]}"; do
ip=${ip//\"/} # Remove quotes
ip=$(echo "$ip" | tr -d '[:space:]') # Remove whitespace
if [[ -n "$ip" ]]; then
block_ip "$ip" "$username"
fi
done
log_message "WARN" "User $username has been completely blocked for $BLOCK_DURATION seconds"
}
# --- Check for and unblock expired IPs ---
check_expired_blocks() {
local current_time=$(date +%s)
local ip username expiry
# Check each line in the block list
while IFS=, read -r ip username expiry || [ -n "$ip" ]; do
if [[ -n "$ip" && -n "$expiry" ]]; then
if (( current_time >= expiry )); then
unblock_ip "$ip"
log_message "INFO" "Auto-unblocked IP $ip for user $username (block expired)"
fi
fi
done < "$BLOCK_LIST"
}
# --- Check if a user has exceeded the IP limit ---
check_ip_limit() {
local username="$1"
local ips=()
# Get all IPs for this user
if command -v jq &>/dev/null; then
readarray -t ips < <(jq -r --arg user "$username" '.[$user][]' "$CONNECTIONS_FILE" 2>/dev/null)
else
# Basic extraction without jq (less reliable)
ip_list=$(grep -oP "\"$username\":\s*\[\K[^\]]*" "$CONNECTIONS_FILE")
IFS=',' read -ra ip_entries <<< "$ip_list"
for entry in "${ip_entries[@]}"; do
# Extract IP from the JSON array entry
ip=$(echo "$entry" | grep -oP '".*"' | tr -d '"' | tr -d '[:space:]')
if [[ -n "$ip" ]]; then
ips+=("$ip")
fi
done
fi
ip_count=${#ips[@]}
# If the user has more IPs than allowed, block ALL their IPs
if (( ip_count > MAX_IPS )); then
log_message "WARN" "User $username has $ip_count IPs (max: $MAX_IPS) - blocking all IPs"
block_all_user_ips "$username"
fi
}
# --- Parse log lines for connections and disconnections ---
parse_log_line() {
local log_line="$1"
local ip_address=""
local username=""
# Extract IP address and username
ip_address=$(echo "$log_line" | grep -oP '"addr": "([^:]+)' | cut -d'"' -f4)
username=$(echo "$log_line" | grep -oP '"id": "([^">]+)' | cut -d'"' -f4)
if [[ -n "$username" && -n "$ip_address" ]]; then
if echo "$log_line" | grep -q "client connected"; then
# Check if this IP is in the block list
if grep -q "^$ip_address," "$BLOCK_LIST"; then
log_message "WARN" "Rejected connection from blocked IP $ip_address for user $username"
# Make sure the IP is still blocked in iptables
if ! iptables -C INPUT -s "$ip_address" -j DROP 2>/dev/null; then
iptables -I INPUT -s "$ip_address" -j DROP
fi
else
update_json "$username" "$ip_address"
check_ip_limit "$username"
fi
elif echo "$log_line" | grep -q "client disconnected"; then
remove_ip "$username" "$ip_address"
# Note: We don't unblock on disconnect - only on block expiration
fi
fi
}
# --- Install Systemd Service ---
install_service() {
cat <<EOF > /etc/systemd/system/${SERVICE_NAME}
[Unit]
Description=Hysteria2 IP Limiter
After=network.target hysteria-server.service
Requires=hysteria-server.service
[Service]
Type=simple
ExecStart=/bin/bash ${SCRIPT_PATH} run
Restart=always
RestartSec=5
User=root
[Install]
WantedBy=multi-user.target.target
EOF
systemctl daemon-reload
systemctl enable ${SERVICE_NAME}
systemctl start ${SERVICE_NAME}
log_message "INFO" "IP Limiter service started"
}
# --- Uninstall Systemd Service ---
uninstall_service() {
systemctl stop ${SERVICE_NAME} 2>/dev/null
systemctl disable ${SERVICE_NAME} 2>/dev/null
rm -f /etc/systemd/system/${SERVICE_NAME}
systemctl daemon-reload
log_message "INFO" "IP Limiter service stopped and removed"
}
# --- Change Configuration ---
change_config() {
local new_block_duration="$1"
local new_max_ips="$2"
if [[ -n "$new_block_duration" ]]; then
if ! [[ "$new_block_duration" =~ ^[0-9]+$ ]]; then
log_message "ERROR" "Invalid block duration: '$new_block_duration'. Must be a number."
return 1
fi
sed -i "s/^BLOCK_DURATION=.*/BLOCK_DURATION=$new_block_duration/" "$CONFIG_ENV"
BLOCK_DURATION=$new_block_duration
log_message "INFO" "Block duration updated to $BLOCK_DURATION seconds"
fi
if [[ -n "$new_max_ips" ]]; then
if ! [[ "$new_max_ips" =~ ^[0-9]+$ ]]; then
log_message "ERROR" "Invalid max IPs: '$new_max_ips'. Must be a number."
return 1
fi
sed -i "s/^MAX_IPS=.*/MAX_IPS=$new_max_ips/" "$CONFIG_ENV"
MAX_IPS=$new_max_ips
log_message "INFO" "Max IPs per user updated to $MAX_IPS"
fi
if systemctl is-active --quiet ${SERVICE_NAME}; then
systemctl restart ${SERVICE_NAME}
log_message "INFO" "IP Limiter service restarted to apply new configuration"
fi
}
# --- Check if running as root ---
if [[ $EUID -ne 0 ]]; then
echo "Error: This script must be run as root for iptables functionality."
exit 1
fi
# --- Check for jq and warn if not available ---
if ! command -v jq &>/dev/null; then
log_message "WARN" "'jq' is not installed. JSON handling may be less reliable."
log_message "WARN" "Consider installing jq with: apt install jq (for Debian/Ubuntu)"
fi
# --- Command execution based on arguments ---
case "$1" in
start)
install_service
;;
stop)
uninstall_service
;;
config)
change_config "$2" "$3"
;;
run)
log_message "INFO" "Monitoring Hysteria server connections. Max IPs per user: $MAX_IPS"
log_message "INFO" "Block duration: $BLOCK_DURATION seconds"
log_message "INFO" "Connection data saved to: $CONNECTIONS_FILE"
log_message "INFO" "Press Ctrl+C to exit"
log_message "INFO" "--------------------------------------------------------"
# Background process to check for expired blocks every 10 seconds
(
while true; do
check_expired_blocks
sleep 10
done
) &
CHECKER_PID=$!
# Cleanup function
cleanup() {
log_message "INFO" "Stopping IP limiter..."
kill $CHECKER_PID 2>/dev/null
exit 0
}
# Set trap for cleanup
trap cleanup SIGINT SIGTERM
# Monitor log for connections and disconnections
journalctl -u hysteria-server.service -f | while read -r line; do
if echo "$line" | grep -q "client connected\|client disconnected"; then
parse_log_line "$line"
fi
done
;;
*)
echo "Usage: $0 {start|stop|config|run} [block_duration] [max_ips]"
exit 1
;;
esac
exit 0

View File

@ -11,3 +11,6 @@ ONLINE_API_URL="http://127.0.0.1:25413/online"
LOCALVERSION="/etc/hysteria/VERSION" LOCALVERSION="/etc/hysteria/VERSION"
LATESTVERSION="https://raw.githubusercontent.com/ReturnFI/Hysteria2/main/VERSION" LATESTVERSION="https://raw.githubusercontent.com/ReturnFI/Hysteria2/main/VERSION"
LASTESTCHANGE="https://raw.githubusercontent.com/ReturnFI/Hysteria2/main/changelog" LASTESTCHANGE="https://raw.githubusercontent.com/ReturnFI/Hysteria2/main/changelog"
CONNECTIONS_FILE="/etc/hysteria/hysteria_connections.json"
BLOCK_LIST="/tmp/hysteria_blocked_ips.txt"
SCRIPT_PATH="/etc/hysteria/core/scripts/hysteria2/limit.sh"

View File

@ -7,6 +7,7 @@ declare -a services=(
"hysteria-telegram-bot.service" "hysteria-telegram-bot.service"
"hysteria-normal-sub.service" "hysteria-normal-sub.service"
"hysteria-singbox.service" "hysteria-singbox.service"
"hysteria-ip-limit.service"
"wg-quick@wgcf.service" "wg-quick@wgcf.service"
) )

View File

@ -1,6 +1,6 @@
from fastapi import APIRouter, HTTPException, UploadFile, File from fastapi import APIRouter, HTTPException, UploadFile, File
from ..schema.config.hysteria import ConfigFile, GetPortResponse, GetSniResponse from ..schema.config.hysteria import ConfigFile, GetPortResponse, GetSniResponse
from ..schema.response import DetailResponse from ..schema.response import DetailResponse, IPLimitConfig
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
import shutil import shutil
import zipfile import zipfile
@ -11,47 +11,6 @@ import cli_api
router = APIRouter() router = APIRouter()
# Change: Installing and uninstalling Hysteria2 is possible only through the CLI
# @router.post('/install', response_model=DetailResponse, summary='Install Hysteria2')
# async def install(body: InstallInputBody):
# """
# Installs Hysteria2 on the given port and uses the provided or default SNI value.
# Args:
# body: An instance of InstallInputBody containing the new port and SNI value.
# Returns:
# A DetailResponse with a message indicating the Hysteria2 installation was successful.
# Raises:
# HTTPException: if an error occurs while installing Hysteria2.
# """
# try:
# cli_api.install_hysteria2(body.port, body.sni)
# return DetailResponse(detail=f'Hysteria2 installed successfully on port {body.port} with SNI {body.sni}.')
# except Exception as e:
# raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
# @router.delete('/uninstall', response_model=DetailResponse, summary='Uninstall Hysteria2')
# async def uninstall():
# """
# Uninstalls Hysteria2.
# Returns:
# A DetailResponse with a message indicating the Hysteria2 uninstallation was successful.
# Raises:
# HTTPException: if an error occurs while uninstalling Hysteria2.
# """
# try:
# cli_api.uninstall_hysteria2()
# return DetailResponse(detail='Hysteria2 uninstalled successfully.')
# except Exception as e:
# raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
@router.patch('/update', response_model=DetailResponse, summary='Update Hysteria2') @router.patch('/update', response_model=DetailResponse, summary='Update Hysteria2')
async def update(): async def update():
""" """
@ -343,3 +302,35 @@ async def set_file(body: ConfigFile):
return DetailResponse(detail='Hysteria2 configuration file updated successfully.') return DetailResponse(detail='Hysteria2 configuration file updated successfully.')
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f'Error: {str(e)}') raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
@router.post('/ip-limit/start', response_model=DetailResponse, summary='Start IP Limiter Service')
async def start_ip_limit_api():
"""Starts the IP Limiter service."""
try:
cli_api.start_ip_limiter()
return DetailResponse(detail='IP Limiter service started successfully.')
except Exception as e:
raise HTTPException(status_code=400, detail=f'Error starting IP Limiter: {str(e)}')
@router.post('/ip-limit/stop', response_model=DetailResponse, summary='Stop IP Limiter Service')
async def stop_ip_limit_api():
"""Stops the IP Limiter service."""
try:
cli_api.stop_ip_limiter()
return DetailResponse(detail='IP Limiter service stopped successfully.')
except Exception as e:
raise HTTPException(status_code=400, detail=f'Error stopping IP Limiter: {str(e)}')
@router.post('/ip-limit/config', response_model=DetailResponse, summary='Configure IP Limiter')
async def config_ip_limit_api(config: IPLimitConfig):
"""Configures the IP Limiter service parameters."""
try:
cli_api.config_ip_limiter(config.block_duration, config.max_ips)
details = 'IP Limiter configuration updated successfully.'
if config.block_duration is not None:
details += f' Block Duration: {config.block_duration} seconds.'
if config.max_ips is not None:
details += f' Max IPs per user: {config.max_ips}.'
return DetailResponse(detail=details)
except Exception as e:
raise HTTPException(status_code=400, detail=f'Error configuring IP Limiter: {str(e)}')

View File

@ -1,5 +1,10 @@
from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
class DetailResponse(BaseModel): class DetailResponse(BaseModel):
detail: str detail: str
class IPLimitConfig(BaseModel):
block_duration: Optional[int] = None
max_ips: Optional[int] = None

View File

@ -19,6 +19,7 @@ class ServerStatusResponse(BaseModel):
class ServerServicesStatusResponse(BaseModel): class ServerServicesStatusResponse(BaseModel):
hysteria_server: bool hysteria_server: bool
hysteria_webpanel: bool hysteria_webpanel: bool
hysteria_iplimit: bool
hysteria_singbox: bool hysteria_singbox: bool
hysteria_normal_sub: bool hysteria_normal_sub: bool
hysteria_telegram_bot: bool hysteria_telegram_bot: bool

View File

@ -133,6 +133,8 @@ def __parse_services_status(services_status: dict[str, bool]) -> ServerServicesS
for service, status in services_status.items(): for service, status in services_status.items():
if 'hysteria-server' in service: if 'hysteria-server' in service:
parsed_services_status['hysteria_server'] = status parsed_services_status['hysteria_server'] = status
elif 'hysteria-ip-limit' in service:
parsed_services_status['hysteria_iplimit'] = status
elif 'hysteria-webpanel' in service: elif 'hysteria-webpanel' in service:
parsed_services_status['hysteria_webpanel'] = status parsed_services_status['hysteria_webpanel'] = status
elif 'telegram-bot' in service: elif 'telegram-bot' in service:
@ -175,7 +177,12 @@ async def check_version_info():
return VersionCheckResponse(is_latest=is_latest, current_version=current_version, return VersionCheckResponse(is_latest=is_latest, current_version=current_version,
latest_version=latest_version, changelog=changelog) latest_version=latest_version, changelog=changelog)
else: else:
return VersionCheckResponse(is_latest=True, current_version=current_version) return VersionCheckResponse(
is_latest=True,
current_version=current_version,
latest_version=current_version,
changelog="Panel is up-to-date."
)
raise HTTPException(status_code=404, detail="Version information not found") raise HTTPException(status_code=404, detail="Version information not found")

View File

@ -90,10 +90,10 @@
</div> </div>
</div> </div>
<div class="col-lg-3 col-6"> <div class="col-lg-3 col-6">
<div class="small-box" id="singbox-status-box"> <div class="small-box" id="iplimit-status-box">
<div class="inner"> <div class="inner">
<h3 id="singbox-status">--</h3> <h3 id="iplimit-status">--</h3>
<p>Singbox</p> <p>IP Limit</p>
</div> </div>
<div class="icon"> <div class="icon">
<i class="fas fa-box"></i> <i class="fas fa-box"></i>
@ -104,7 +104,7 @@
<div class="small-box" id="normalsub-status-box"> <div class="small-box" id="normalsub-status-box">
<div class="inner"> <div class="inner">
<h3 id="normalsub-status">--</h3> <h3 id="normalsub-status">--</h3>
<p>Normalsub</p> <p>Normal Subscription</p>
</div> </div>
<div class="icon"> <div class="icon">
<i class="fas fa-rss"></i> <i class="fas fa-rss"></i>
@ -139,7 +139,7 @@
updateServiceBox('hysteria2', data.hysteria_server); updateServiceBox('hysteria2', data.hysteria_server);
updateServiceBox('telegrambot', data.hysteria_telegram_bot); updateServiceBox('telegrambot', data.hysteria_telegram_bot);
updateServiceBox('singbox', data.hysteria_singbox); updateServiceBox('iplimit', data.hysteria_iplimit);
updateServiceBox('normalsub', data.hysteria_normal_sub); updateServiceBox('normalsub', data.hysteria_normal_sub);
}) })
.catch(error => console.error('Error fetching service statuses:', error)); .catch(error => console.error('Error fetching service statuses:', error));

View File

@ -53,6 +53,12 @@
aria-controls='backup' aria-selected='false'><i class="fas fa-download"></i> aria-controls='backup' aria-selected='false'><i class="fas fa-download"></i>
Backup</a> Backup</a>
</li> </li>
<!-- IP Limiter Tab -->
<li class='nav-item'>
<a class='nav-link' id='ip-limit-tab' data-toggle='pill' href='#ip-limit' role='tab'
aria-controls='ip-limit' aria-selected='false'><i class="fas fa-user-slash"></i>
IP Limit</a>
</li>
</ul> </ul>
</div> </div>
<div class='card-body' style="margin-left: 25px;"> <div class='card-body' style="margin-left: 25px;">
@ -76,7 +82,7 @@
<!-- SingBox Sub Tab --> <!-- SingBox Sub Tab -->
<div class='tab-pane fade show active' id='singbox' role='tabpanel' <div class='tab-pane fade show active' id='singbox' role='tabpanel'
aria-labelledby='singbox-tab'> aria-labelledby='singbox-tab'>
<form> <form id="singbox">
<div class='form-group'> <div class='form-group'>
<label for='singbox_domain'>Domain:</label> <label for='singbox_domain'>Domain:</label>
<input type='text' class='form-control' id='singbox_domain' <input type='text' class='form-control' id='singbox_domain'
@ -105,7 +111,7 @@
<!-- Normal Sub Tab --> <!-- Normal Sub Tab -->
<div class='tab-pane fade' id='normal' role='tabpanel' aria-labelledby='normal-tab'> <div class='tab-pane fade' id='normal' role='tabpanel' aria-labelledby='normal-tab'>
<form> <form id="normal">
<div class='form-group'> <div class='form-group'>
<label for='normal_domain'>Domain:</label> <label for='normal_domain'>Domain:</label>
<input type='text' class='form-control' id='normal_domain' <input type='text' class='form-control' id='normal_domain'
@ -137,7 +143,7 @@
<!-- Telegram Bot Tab --> <!-- Telegram Bot Tab -->
<div class='tab-pane fade' id='telegram' role='tabpanel' aria-labelledby='telegram-tab'> <div class='tab-pane fade' id='telegram' role='tabpanel' aria-labelledby='telegram-tab'>
<form> <form id="telegram">
<div class='form-group'> <div class='form-group'>
<label for='telegram_api_token'>API Token:</label> <label for='telegram_api_token'>API Token:</label>
<input type='text' class='form-control' id='telegram_api_token' <input type='text' class='form-control' id='telegram_api_token'
@ -154,7 +160,8 @@
Please enter a valid Admin ID. Please enter a valid Admin ID.
</div> </div>
</div> </div>
<button id="telegram_start" type='button' class='btn btn-success'>Start</button> <button id="telegram_start" type='button' class='btn btn-success'>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>Start</button>
<button id="telegram_stop" type='button' class='btn btn-danger' <button id="telegram_stop" type='button' class='btn btn-danger'
style="display: none;">Stop</button> style="display: none;">Stop</button>
@ -164,7 +171,7 @@
<!-- Port Tab --> <!-- Port Tab -->
<div class='tab-pane fade' id='port' role='tabpanel' aria-labelledby='port-tab'> <div class='tab-pane fade' id='port' role='tabpanel' aria-labelledby='port-tab'>
<form> <form id="port">
<div class='form-group'> <div class='form-group'>
<label for='hysteria_port'>Port:</label> <label for='hysteria_port'>Port:</label>
<input type='text' class='form-control' id='hysteria_port' <input type='text' class='form-control' id='hysteria_port'
@ -180,7 +187,7 @@
<!-- SNI Tab --> <!-- SNI Tab -->
<div class='tab-pane fade' id='sni' role='tabpanel' aria-labelledby='sni-tab'> <div class='tab-pane fade' id='sni' role='tabpanel' aria-labelledby='sni-tab'>
<form> <form id="sni">
<div class='form-group'> <div class='form-group'>
<label for='sni_domain'>Domain:</label> <label for='sni_domain'>Domain:</label>
<input type='text' class='form-control' id='sni_domain' <input type='text' class='form-control' id='sni_domain'
@ -195,7 +202,7 @@
<!-- Change IP Tab --> <!-- Change IP Tab -->
<div class='tab-pane fade' id='change_ip' role='tabpanel' aria-labelledby='ip-tab'> <div class='tab-pane fade' id='change_ip' role='tabpanel' aria-labelledby='ip-tab'>
<form> <form id="change_ip">
<div class='form-group'> <div class='form-group'>
<label for='ipv4'>IPv4:</label> <label for='ipv4'>IPv4:</label>
<input type='text' class='form-control' id='ipv4' placeholder='Enter IPv4' <input type='text' class='form-control' id='ipv4' placeholder='Enter IPv4'
@ -230,6 +237,59 @@
<div id="backup_status" class="mt-2"></div> <!-- Status messages --> <div id="backup_status" class="mt-2"></div> <!-- Status messages -->
</div> </div>
<!-- IP Limit Tab (New) -->
<div class='tab-pane fade' id='ip-limit' role='tabpanel' aria-labelledby='ip-limit-tab'>
<ul class='nav nav-tabs' id='ip-limit-tabs' role='tablist'>
<li class='nav-item'>
<a class='nav-link active' id='ip-limit-service-tab' data-toggle='tab' href='#ip-limit-service'
role='tab' aria-controls='ip-limit-service'
aria-selected='true'><strong>Service Control</strong></a>
</li>
<li class='nav-item ip-limit-config-tab-li' style="display: none;">
<a class='nav-link' id='ip-limit-config-tab' data-toggle='tab' href='#ip-limit-config-content' role='tab'
aria-controls='ip-limit-config-content' aria-selected='false'><strong>Configuration</strong></a>
</li>
</ul>
<div class='tab-content' id='ip-limit-tabs-content'>
<br>
<!-- IP Limit Service Control Sub Tab -->
<div class='tab-pane fade show active' id='ip-limit-service' role='tabpanel'
aria-labelledby='ip-limit-service-tab'>
<form id="ip_limit_service_form">
<button id="ip_limit_start" type='button' class='btn btn-success'>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
Start
</button>
<button id="ip_limit_stop" type='button' class='btn btn-danger' style="display: none;">Stop</button>
</form>
</div>
<!-- IP Limit Configuration Sub Tab -->
<div class='tab-pane fade' id='ip-limit-config-content' role='tabpanel' aria-labelledby='ip-limit-config-tab'>
<form id="ip_limit_config">
<div class='form-group'>
<label for='block_duration'>Block Duration (seconds):</label>
<input type='text' class='form-control' id='block_duration'
placeholder='Enter Block Duration' value="">
<div class="invalid-feedback">
Please enter a valid positive number for block duration.
</div>
</div>
<div class='form-group'>
<label for='max_ips'>Max IPs per User:</label>
<input type='text' class='form-control' id='max_ips'
placeholder='Enter Max IPs per User' value="">
<div class="invalid-feedback">
Please enter a valid positive number for max IPs.
</div>
</div>
<button id="ip_limit_change_config" type='button' class='btn btn-primary'>Save Configuration</button>
</form>
</div>
</div>
</div>
</div> </div>
</div> </div>
<!-- /.card --> <!-- /.card -->
@ -268,7 +328,7 @@
if (!input) return true; if (!input) return true;
const ipV4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; const ipV4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const ipV6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}([0-9a-fA-F]{1,4}|:)|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/; const ipV6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}([0-9a-fA-F]{1,4}|:)|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,2}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,3}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,5}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,6}){1,6})|:((:[0-9a-fA-F]{1,7}){1,7}|:))$/;
const domainRegex = /^(?!\-)(?:[a-zA-Z\d\-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+)[a-zA-Z\d]{1,63}$/; const domainRegex = /^(?!\-)(?:[a-zA-Z\d\-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+)[a-zA-Z\d]{1,63}$/;
const lowerInput = input.toLowerCase(); const lowerInput = input.toLowerCase();
@ -279,6 +339,11 @@
return false; return false;
} }
function isValidPositiveNumber(value) {
if (!value) return false;
return /^[0-9]+$/.test(value) && parseInt(value) > 0;
}
function confirmAction(actionName, callback) { function confirmAction(actionName, callback) {
Swal.fire({ Swal.fire({
@ -356,14 +421,23 @@
} else { } else {
input.removeClass('is-invalid'); input.removeClass('is-invalid');
} }
} else if (id === 'ipv4' || id === 'ipv6') { // Apply isValidIPorDomain for both IPv4 and IPv6 } else if (id === 'ipv4' || id === 'ipv6') {
if (!isValidIPorDomain(input.val())) { if (!isValidIPorDomain(input.val())) {
input.addClass('is-invalid'); input.addClass('is-invalid');
isValid = false; isValid = false;
} else { } else {
input.removeClass('is-invalid'); input.removeClass('is-invalid');
} }
} else { } else if (id === 'block_duration' || id === 'max_ips') {
if (!isValidPositiveNumber(input.val())) {
input.addClass('is-invalid');
isValid = false;
} else {
input.removeClass('is-invalid');
}
}
else {
if (!input.val().trim()) { if (!input.val().trim()) {
input.addClass('is-invalid'); input.addClass('is-invalid');
@ -394,16 +468,18 @@
const servicesMap = { const servicesMap = {
"hysteria_telegram_bot": "#telegram", "hysteria_telegram_bot": "#telegram",
"hysteria_singbox": "#singbox", "hysteria_singbox": "#singbox",
"hysteria_normal_sub": "#normal" "hysteria_normal_sub": "#normal",
"hysteria_iplimit": "#ip-limit-service"
}; };
Object.keys(servicesMap).forEach(service => { Object.keys(servicesMap).forEach(service => {
let selector = servicesMap[service]; let selector = servicesMap[service];
let isRunning = data[service]; let isRunning = data[service];
let serviceName = service.replace("hysteria_", "");
if (isRunning) { if (isRunning) {
$(selector + " input, " + selector + " label").remove(); $(selector + " input, " + selector + " label").remove();
$(selector + " .btn-success").remove(); $(selector + " .btn-success").hide();
$(selector).prepend(`<div class='alert alert-info'>Service is running. You can stop it if needed.</div>`); $(selector).prepend(`<div class='alert alert-info'>Service is running. You can stop it if needed.</div>`);
$(selector + " .btn-danger").show(); $(selector + " .btn-danger").show();
@ -416,6 +492,16 @@
if(service === "hysteria_normal_sub"){ if(service === "hysteria_normal_sub"){
$("#normal_start").prop('disabled', true); $("#normal_start").prop('disabled', true);
} }
if(service === "hysteria_iplimit"){
$("#ip_limit_start").hide();
$("#ip_limit_stop").show();
$(".ip-limit-config-tab-li").show();
if (!$("#ip-limit-config-tab").hasClass('active')) {
$('#ip-limit-service-tab').tab('show');
}
}
} else { } else {
$(selector + " input, " + selector + " label").show(); $(selector + " input, " + selector + " label").show();
@ -431,6 +517,13 @@
if(service === "hysteria_normal_sub"){ if(service === "hysteria_normal_sub"){
$("#normal_start").prop('disabled', false); $("#normal_start").prop('disabled', false);
} }
if(service === "hysteria_iplimit"){
$("#ip_limit_start").show();
$("#ip_limit_stop").hide();
$(".ip-limit-config-tab-li").hide();
$('#ip-limit-service-tab').tab('show');
}
} }
}); });
} }
@ -662,7 +755,39 @@
}); });
}); });
} }
function startIPLimit() {
sendRequest(
"{{ url_for('start_ip_limit_api') }}",
"POST",
null,
"IP Limit service started successfully!",
"#ip_limit_start"
);
}
function stopIPLimit() {
sendRequest(
"{{ url_for('stop_ip_limit_api') }}",
"POST",
null,
"IP Limit service stopped successfully!",
null
);
}
function configIPLimit() {
if (!validateForm('ip_limit_config')) return;
const blockDuration = $("#block_duration").val();
const maxIps = $("#max_ips").val();
sendRequest(
"{{ url_for('config_ip_limit_api') }}",
"POST",
{ block_duration: blockDuration, max_ips: maxIps },
"IP Limit configuration saved successfully!",
"#ip_limit_change_config",
false
);
}
$("#telegram_start").on("click", startTelegram); $("#telegram_start").on("click", startTelegram);
@ -676,6 +801,10 @@
$("#ip_change").on("click", saveIP); $("#ip_change").on("click", saveIP);
$("#download_backup").on("click", downloadBackup); $("#download_backup").on("click", downloadBackup);
$("#upload_backup").on("click", uploadBackup); $("#upload_backup").on("click", uploadBackup);
$("#ip_limit_start").on("click", startIPLimit);
$("#ip_limit_stop").on("click", stopIPLimit);
$("#ip_limit_change_config").on("click", configIPLimit);
$('#singbox_domain, #normal_domain, #sni_domain').on('input', function () { $('#singbox_domain, #normal_domain, #sni_domain').on('input', function () {
if (isValidDomain($(this).val())) { if (isValidDomain($(this).val())) {
@ -692,7 +821,7 @@
$(this).addClass('is-invalid'); $(this).addClass('is-invalid');
} }
}); });
$('#ipv4, #ipv6').on('input', function () { // Apply to both ipv4 and ipv6 $('#ipv4, #ipv6').on('input', function () {
if (isValidIPorDomain($(this).val())) { if (isValidIPorDomain($(this).val())) {
$(this).removeClass('is-invalid'); $(this).removeClass('is-invalid');
} else { } else {
@ -707,6 +836,13 @@
$(this).addClass('is-invalid'); $(this).addClass('is-invalid');
} }
}); });
$('#block_duration, #max_ips').on('input', function () {
if (isValidPositiveNumber($(this).val())) {
$(this).removeClass('is-invalid');
} else {
$(this).addClass('is-invalid');
}
});
}); });
</script> </script>
{% endblock %} {% endblock %}

95
menu.sh
View File

@ -805,6 +805,95 @@ masquerade_handler() {
done done
} }
ip_limit_handler() {
while true; do
echo -e "${cyan}1.${NC} Start IP Limiter Service"
echo -e "${red}2.${NC} Stop IP Limiter Service"
echo -e "${yellow}3.${NC} Change IP Limiter Configuration"
echo "0. Back"
read -p "Choose an option: " option
case $option in
1)
if systemctl is-active --quiet hysteria-ip-limit.service; then
echo "The hysteria-ip-limit.service is already active."
else
while true; do
read -e -p "Enter Block Duration (seconds, default: 60): " block_duration
block_duration=${block_duration:-60} # Default to 60 if empty
if ! [[ "$block_duration" =~ ^[0-9]+$ ]]; then
echo "Invalid Block Duration. Please enter a number."
else
break
fi
done
while true; do
read -e -p "Enter Max IPs per User (default: 1): " max_ips
max_ips=${max_ips:-1} # Default to 1 if empty
if ! [[ "$max_ips" =~ ^[0-9]+$ ]]; then
echo "Invalid Max IPs. Please enter a number."
else
break
fi
done
python3 $CLI_PATH config-ip-limit --block-duration "$block_duration" --max-ips "$max_ips"
python3 $CLI_PATH start-ip-limit
fi
;;
2)
if ! systemctl is-active --quiet hysteria-ip-limit.service; then
echo "The hysteria-ip-limit.service is already inactive."
else
python3 $CLI_PATH stop-ip-limit
fi
;;
3)
block_duration=""
max_ips=""
updated=false
while true; do
read -e -p "Enter New Block Duration (seconds, current: $(grep '^BLOCK_DURATION=' /etc/hysteria/.configs.env | cut -d'=' -f2), leave empty to keep current): " input_block_duration
if [[ -n "$input_block_duration" ]] && ! [[ "$input_block_duration" =~ ^[0-9]+$ ]]; then
echo "Invalid Block Duration. Please enter a number or leave empty."
else
if [[ -n "$input_block_duration" ]]; then
block_duration="$input_block_duration"
updated=true
fi
break
fi
done
while true; do
read -e -p "Enter New Max IPs per User (current: $(grep '^MAX_IPS=' /etc/hysteria/.configs.env | cut -d'=' -f2), leave empty to keep current): " input_max_ips
if [[ -n "$input_max_ips" ]] && ! [[ "$input_max_ips" =~ ^[0-9]+$ ]]; then
echo "Invalid Max IPs. Please enter a number or leave empty."
else
if [[ -n "$input_max_ips" ]]; then
max_ips="$input_max_ips"
updated=true
fi
break
fi
done
if [[ "$updated" == "true" ]]; then
python3 $CLI_PATH config-ip-limit --block-duration "$block_duration" --max-ips "$max_ips"
else
echo "No changes to IP Limiter configuration were provided."
fi
;;
0)
break
;;
*)
echo "Invalid option. Please try again."
;;
esac
done
}
# Function to display the main menu # Function to display the main menu
display_main_menu() { display_main_menu() {
@ -933,7 +1022,8 @@ display_advance_menu() {
echo -e "${cyan}[14] ${NC}↝ Manage Masquerade" echo -e "${cyan}[14] ${NC}↝ Manage Masquerade"
echo -e "${cyan}[15] ${NC}↝ Restart Hysteria2" echo -e "${cyan}[15] ${NC}↝ Restart Hysteria2"
echo -e "${cyan}[16] ${NC}↝ Update Core Hysteria2" echo -e "${cyan}[16] ${NC}↝ Update Core Hysteria2"
echo -e "${red}[17] ${NC}Uninstall Hysteria2" echo -e "${cyan}[17] ${NC}IP Limiter Menu"
echo -e "${red}[18] ${NC}↝ Uninstall Hysteria2"
echo -e "${red}[0] ${NC}↝ Back to Main Menu" echo -e "${red}[0] ${NC}↝ Back to Main Menu"
echo -e "${LPurple}◇──────────────────────────────────────────────────────────────────────◇${NC}" echo -e "${LPurple}◇──────────────────────────────────────────────────────────────────────◇${NC}"
echo -ne "${yellow}➜ Enter your option: ${NC}" echo -ne "${yellow}➜ Enter your option: ${NC}"
@ -963,7 +1053,8 @@ advance_menu() {
14) masquerade_handler ;; 14) masquerade_handler ;;
15) python3 $CLI_PATH restart-hysteria2 ;; 15) python3 $CLI_PATH restart-hysteria2 ;;
16) python3 $CLI_PATH update-hysteria2 ;; 16) python3 $CLI_PATH update-hysteria2 ;;
17) python3 $CLI_PATH uninstall-hysteria2 ;; 17) ip_limit_handler ;;
18) python3 $CLI_PATH uninstall-hysteria2 ;;
0) return ;; 0) return ;;
*) echo "Invalid option. Please try again." ;; *) echo "Invalid option. Please try again." ;;
esac esac