#!/bin/bash
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
C_RESET=$'\033[0m'
C_BOLD=$'\033[1m'
C_DIM=$'\033[2m'
C_UL=$'\033[4m'
# Premium Color Palette
C_RED=$'\033[38;5;196m' # Bright Red
C_GREEN=$'\033[38;5;46m' # Neon Green
C_YELLOW=$'\033[38;5;226m' # Bright Yellow
C_BLUE=$'\033[38;5;39m' # Deep Sky Blue
C_PURPLE=$'\033[38;5;135m' # Light Purple
C_CYAN=$'\033[38;5;51m' # Cyan
C_WHITE=$'\033[38;5;255m' # Bright White
C_GRAY=$'\033[38;5;245m' # Gray
C_ORANGE=$'\033[38;5;208m' # Orange
# Semantic Aliases
C_TITLE=$C_PURPLE
C_CHOICE=$C_CYAN
C_PROMPT=$C_BLUE
C_WARN=$C_YELLOW
C_DANGER=$C_RED
C_STATUS_A=$C_GREEN
C_STATUS_I=$C_GRAY
C_ACCENT=$C_ORANGE
DB_DIR="/etc/firewallfalcon"
DB_FILE="$DB_DIR/users.db"
INSTALL_FLAG_FILE="$DB_DIR/.install"
BADVPN_SERVICE_FILE="/etc/systemd/system/badvpn.service"
BADVPN_BUILD_DIR="/root/badvpn-build"
HAPROXY_CONFIG="/etc/haproxy/haproxy.cfg"
NGINX_CONFIG_FILE="/etc/nginx/sites-available/default"
SSL_CERT_DIR="/etc/firewallfalcon/ssl"
SSL_CERT_FILE="$SSL_CERT_DIR/firewallfalcon.pem"
SSL_CERT_CHAIN_FILE="$SSL_CERT_DIR/firewallfalcon.crt"
SSL_CERT_KEY_FILE="$SSL_CERT_DIR/firewallfalcon.key"
EDGE_CERT_INFO_FILE="$DB_DIR/edge_cert.conf"
NGINX_PORTS_FILE="$DB_DIR/nginx_ports.conf"
EDGE_PUBLIC_HTTP_PORT="80"
EDGE_PUBLIC_TLS_PORT="443"
NGINX_INTERNAL_HTTP_PORT="8880"
NGINX_INTERNAL_TLS_PORT="8443"
HAPROXY_INTERNAL_DECRYPT_PORT="10443"
DNSTT_SERVICE_FILE="/etc/systemd/system/dnstt.service"
DNSTT_BINARY="/usr/local/bin/dnstt-server"
DNSTT_KEYS_DIR="/etc/firewallfalcon/dnstt"
DNSTT_CONFIG_FILE="$DB_DIR/dnstt_info.conf"
DNS_INFO_FILE="$DB_DIR/dns_info.conf"
UDP_CUSTOM_DIR="/root/udp"
UDP_CUSTOM_SERVICE_FILE="/etc/systemd/system/udp-custom.service"
UDPGW_BINARY="/usr/local/bin/udpgw"
UDPGW_SERVICE_FILE="/etc/systemd/system/udpgw.service"
SSH_BANNER_FILE="/etc/bannerssh"
FALCONPROXY_SERVICE_FILE="/etc/systemd/system/falconproxy.service"
FALCONPROXY_BINARY="/usr/local/bin/falconproxy"
FALCONPROXY_CONFIG_FILE="$DB_DIR/falconproxy_config.conf"
LIMITER_SCRIPT="/usr/local/bin/firewallfalcon-limiter.sh"
LIMITER_SERVICE="/etc/systemd/system/firewallfalcon-limiter.service"
BANDWIDTH_DIR="$DB_DIR/bandwidth"
BANDWIDTH_SCRIPT="/usr/local/bin/firewallfalcon-bandwidth.sh"
BANDWIDTH_SERVICE="/etc/systemd/system/firewallfalcon-bandwidth.service"
LEGACY_BANDWIDTH_DIR="/usr/local/bin/firewallfalcon-bandwidth"
TRIAL_CLEANUP_SCRIPT="/usr/local/bin/firewallfalcon-trial-cleanup.sh"
LOGIN_INFO_SCRIPT="/usr/local/bin/firewallfalcon-login-info.sh"
SSHD_FF_CONFIG="/etc/ssh/sshd_config.d/firewallfalcon.conf"
# --- Web Panel Variables ---
PANEL_SCRIPT="/usr/local/bin/firewallfalcon-panel.py"
PANEL_HTML_DIR="$DB_DIR/panel"
PANEL_HTML_FILE="$DB_DIR/panel/index.html"
PANEL_CONF="$DB_DIR/panel.conf"
PANEL_SERVICE_FILE="/etc/systemd/system/firewallfalcon-panel.service"
PANEL_PORT=44380
# Localized offline bundle (remote-independent install). Overridden by install.sh.
# Every first-party asset (panel, udp-custom, udpgw, falconproxy) is read from here;
# there is no remote fallback.
FF_BUNDLE_DIR="${FF_BUNDLE_DIR:-/opt/firewallfalcon-bundle}"
# Abort the caller when a required bundle asset is missing.
ff_require_bundle_file() {
local path="$1"
if [[ ! -f "$path" ]]; then
echo -e "\n${C_RED}❌ Missing bundle file: $path${C_RESET}"
echo -e "${C_YELLOW} The local bundle is incomplete. Re-run install.sh to restore it.${C_RESET}"
return 1
fi
return 0
}
# --- ZiVPN Variables ---
ZIVPN_DIR="/etc/zivpn"
ZIVPN_BIN="/usr/local/bin/zivpn"
ZIVPN_SERVICE_FILE="/etc/systemd/system/zivpn.service"
ZIVPN_CONFIG_FILE="$ZIVPN_DIR/config.json"
ZIVPN_CERT_FILE="$ZIVPN_DIR/zivpn.crt"
ZIVPN_KEY_FILE="$ZIVPN_DIR/zivpn.key"
DESEC_TOKEN="V55cFY8zTictLCPfviiuX5DHjs15"
DESEC_DOMAIN="manager.firewallfalcon.qzz.io"
SELECTED_USER=""
UNINSTALL_MODE="interactive"
BANNER_CACHE_TTL=15
BANNER_CACHE_TS=0
BANNER_CACHE_OS_NAME=""
BANNER_CACHE_UP_TIME=""
BANNER_CACHE_RAM_USAGE=""
BANNER_CACHE_CPU_LOAD=""
BANNER_CACHE_ONLINE_USERS=0
BANNER_CACHE_TOTAL_USERS=0
SSH_SESSION_CACHE_TTL=10
SSH_SESSION_CACHE_TS=0
SSH_SESSION_CACHE_DB_MTIME=0
SSH_SESSION_TOTAL=0
APT_CACHE_READY=0
FF_USERS_GROUP="ffusers"
declare -A SSH_SESSION_COUNTS=()
declare -A SSH_SESSION_PIDS=()
# --- Package Manager Abstraction ---
FF_PKG_MGR=""
_detect_pkg_manager() {
if command -v apt-get &>/dev/null; then
FF_PKG_MGR="apt"
elif command -v dnf &>/dev/null; then
FF_PKG_MGR="dnf"
elif command -v yum &>/dev/null; then
FF_PKG_MGR="yum"
elif command -v zypper &>/dev/null; then
FF_PKG_MGR="zypper"
elif command -v pacman &>/dev/null; then
FF_PKG_MGR="pacman"
else
echo -e "${C_RED}❌ No supported package manager found (apt/dnf/yum/zypper/pacman).${C_RESET}"
exit 1
fi
}
_detect_pkg_manager
_map_pkg_names() {
local -a result=()
local pkg
for pkg in "$@"; do
case "$FF_PKG_MGR" in
dnf|yum)
case "$pkg" in
build-essential) result+=(gcc gcc-c++ make) ;;
libssl-dev) result+=(openssl-devel) ;;
libnspr4-dev) result+=(nspr-devel) ;;
libnss3-dev) result+=(nss-devel) ;;
nginx-common) result+=(nginx) ;;
pkg-config) result+=(pkgconf) ;;
*) result+=("$pkg") ;;
esac ;;
zypper)
case "$pkg" in
build-essential) result+=(gcc gcc-c++ make) ;;
libssl-dev) result+=(libopenssl-devel) ;;
libnspr4-dev) result+=(mozilla-nspr-devel) ;;
libnss3-dev) result+=(mozilla-nss-devel) ;;
nginx-common) result+=(nginx) ;;
*) result+=("$pkg") ;;
esac ;;
pacman)
case "$pkg" in
build-essential) result+=(base-devel) ;;
libssl-dev) result+=(openssl) ;;
libnspr4-dev) result+=(nspr) ;;
libnss3-dev) result+=(nss) ;;
nginx-common) result+=(nginx) ;;
bc) result+=(bc) ;;
*) result+=("$pkg") ;;
esac ;;
*) result+=("$pkg") ;;
esac
done
printf '%s\n' "${result[@]}"
}
if [[ $EUID -ne 0 ]]; then
echo -e "${C_RED}❌ Error: This script requires root privileges to run.${C_RESET}"
exit 1
fi
get_ubuntu_codename() {
local codename=""
if [[ -r /etc/os-release ]]; then
codename=$(awk -F= '/^(VERSION_CODENAME|UBUNTU_CODENAME)=/{gsub(/"/, "", $2); if ($2 != "") { print $2; exit }}' /etc/os-release 2>/dev/null)
fi
if [[ -z "$codename" ]] && command -v lsb_release &>/dev/null; then
codename=$(lsb_release -sc 2>/dev/null)
fi
echo "$codename"
}
is_known_eol_ubuntu_codename() {
case "$1" in
yakkety|zesty|artful|cosmic|disco|eoan|groovy|hirsute|impish|kinetic|lunar|mantic|oracular|plucky)
return 0
;;
*)
return 1
;;
esac
}
rewrite_ubuntu_apt_sources() {
local mode="$1"
local os_id=""
local changed=false
local file backup_file
local from_archive to_archive from_security to_security from_ports to_ports
local -a source_files=("/etc/apt/sources.list" /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources)
if [[ -r /etc/os-release ]]; then
os_id=$(awk -F= '/^ID=/{gsub(/"/, "", $2); print $2; exit}' /etc/os-release 2>/dev/null)
fi
[[ "$os_id" == "ubuntu" ]] || return 1
case "$mode" in
primary)
from_archive='https?://([A-Za-z0-9-]+\.)?archive\.ubuntu\.com/ubuntu'
to_archive='http://archive.ubuntu.com/ubuntu'
from_security='https?://security\.ubuntu\.com/ubuntu'
to_security='http://security.ubuntu.com/ubuntu'
from_ports='https?://ports\.ubuntu\.com/ubuntu-ports'
to_ports='http://ports.ubuntu.com/ubuntu-ports'
;;
old-releases)
from_archive='https?://([A-Za-z0-9-]+\.)?archive\.ubuntu\.com/ubuntu'
to_archive='http://old-releases.ubuntu.com/ubuntu'
from_security='https?://security\.ubuntu\.com/ubuntu'
to_security='http://old-releases.ubuntu.com/ubuntu'
from_ports='https?://ports\.ubuntu\.com/ubuntu-ports'
to_ports='http://old-releases.ubuntu.com/ubuntu'
;;
*)
return 1
;;
esac
for file in "${source_files[@]}"; do
[[ -f "$file" ]] || continue
if grep -Eq "$from_archive|$from_security|$from_ports" "$file" 2>/dev/null; then
backup_file="${file}.bak.firewallfalcon"
[[ -f "$backup_file" ]] || cp "$file" "$backup_file" 2>/dev/null || true
sed -i -E \
-e "s|$from_archive|$to_archive|g" \
-e "s|$from_security|$to_security|g" \
-e "s|$from_ports|$to_ports|g" \
"$file" 2>/dev/null
changed=true
fi
done
$changed
}
repair_ubuntu_apt_mirrors() {
rewrite_ubuntu_apt_sources "primary"
}
switch_ubuntu_to_old_releases() {
local codename
codename=$(get_ubuntu_codename)
[[ -n "$codename" ]] || return 1
is_known_eol_ubuntu_codename "$codename" || return 1
rewrite_ubuntu_apt_sources "old-releases"
}
ff_apt_update() {
local -a apt_opts=(
-o Acquire::Retries=3
-o Acquire::ForceIPv4=true
-o Acquire::http::Timeout=20
-o Acquire::https::Timeout=20
-o Acquire::http::Pipeline-Depth=0
)
if (( APT_CACHE_READY == 1 )); then
return 0
fi
if DEBIAN_FRONTEND=noninteractive apt-get "${apt_opts[@]}" update; then
APT_CACHE_READY=1
return 0
fi
if repair_ubuntu_apt_mirrors; then
echo -e "${C_YELLOW}⚠️ APT mirror timed out. Switching Ubuntu sources to archive.ubuntu.com and retrying...${C_RESET}"
apt-get clean >/dev/null 2>&1 || true
if DEBIAN_FRONTEND=noninteractive apt-get "${apt_opts[@]}" update; then
APT_CACHE_READY=1
return 0
fi
fi
if switch_ubuntu_to_old_releases; then
echo -e "${C_YELLOW}⚠️ Detected an end-of-life Ubuntu release. Switching APT sources to old-releases.ubuntu.com and retrying...${C_RESET}"
apt-get clean >/dev/null 2>&1 || true
if DEBIAN_FRONTEND=noninteractive apt-get "${apt_opts[@]}" update; then
APT_CACHE_READY=1
return 0
fi
fi
echo -e "${C_RED}❌ Failed to refresh package lists. Please check VPS network, DNS, or blocked Ubuntu mirrors.${C_RESET}"
return 1
}
ff_apt_install() {
local -a packages=("$@")
(( ${#packages[@]} > 0 )) || return 0
ff_apt_update || return 1
DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Use-Pty=0 install "${packages[@]}"
}
ff_apt_purge() {
local -a packages=("$@")
(( ${#packages[@]} > 0 )) || return 0
DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Use-Pty=0 purge "${packages[@]}"
}
ff_pkg_install() {
local -a packages=("$@")
(( ${#packages[@]} > 0 )) || return 0
local -a mapped=()
mapfile -t mapped < <(_map_pkg_names "${packages[@]}")
case "$FF_PKG_MGR" in
apt) ff_apt_install "${mapped[@]}" ;;
dnf) dnf install -y -q "${mapped[@]}" ;;
yum) yum install -y -q "${mapped[@]}" ;;
zypper) zypper install -y -q "${mapped[@]}" ;;
pacman) pacman -S --noconfirm --needed "${mapped[@]}" ;;
*) echo -e "${C_RED}❌ Unsupported package manager.${C_RESET}"; return 1 ;;
esac
}
ff_pkg_purge() {
local -a packages=("$@")
(( ${#packages[@]} > 0 )) || return 0
local -a mapped=()
mapfile -t mapped < <(_map_pkg_names "${packages[@]}")
case "$FF_PKG_MGR" in
apt) ff_apt_purge "${mapped[@]}" ;;
dnf) dnf remove -y -q "${mapped[@]}" ;;
yum) yum remove -y -q "${mapped[@]}" ;;
zypper) zypper remove -y "${mapped[@]}" ;;
pacman) pacman -Rns --noconfirm "${mapped[@]}" 2>/dev/null ;;
*) echo -e "${C_RED}❌ Unsupported package manager.${C_RESET}"; return 1 ;;
esac
}
ff_pkg_autoremove() {
case "$FF_PKG_MGR" in
apt) apt-get autoremove -y >/dev/null 2>&1 ;;
dnf) dnf autoremove -y -q >/dev/null 2>&1 ;;
yum) yum autoremove -y -q >/dev/null 2>&1 ;;
zypper) zypper packages --unneeded 2>/dev/null | awk -F'|' 'NR>3{print $3}' | xargs -r zypper remove -y >/dev/null 2>&1 ;;
pacman) pacman -Qdtq 2>/dev/null | xargs -r pacman -Rns --noconfirm >/dev/null 2>&1 ;;
esac
return 0
}
ff_pkg_is_installed() {
local pkg="$1"
case "$FF_PKG_MGR" in
apt) dpkg -s "$pkg" &>/dev/null ;;
dnf|yum) rpm -q "$pkg" &>/dev/null ;;
zypper) rpm -q "$pkg" &>/dev/null ;;
pacman) pacman -Q "$pkg" &>/dev/null ;;
esac
}
# Mandatory Dependency Check (Added jq and curl)
check_environment() {
local missing_packages=()
local cmd
for cmd in bc jq curl wget; do
if ! command -v "$cmd" &> /dev/null; then
missing_packages+=("$cmd")
fi
done
if (( ${#missing_packages[@]} > 0 )); then
echo -e "${C_YELLOW}⚠️ Installing missing dependencies: ${missing_packages[*]}${C_RESET}"
ff_pkg_install "${missing_packages[@]}" >/dev/null 2>&1 || {
echo -e "${C_RED}❌ Error: Failed to install required dependencies: ${missing_packages[*]}.${C_RESET}"
exit 1
}
fi
}
ensure_firewallfalcon_dirs() {
mkdir -p "$DB_DIR" "$SSL_CERT_DIR" "$BANDWIDTH_DIR" /etc/ssh/sshd_config.d
touch "$DB_FILE"
}
ensure_firewallfalcon_system_group() {
getent group "$FF_USERS_GROUP" >/dev/null 2>&1 || groupadd "$FF_USERS_GROUP" >/dev/null 2>&1 || true
}
db_has_user() {
[[ -f "$DB_FILE" ]] || return 1
awk -F: -v target="$1" '$1 == target { found=1; exit } END { exit(found ? 0 : 1) }' "$DB_FILE"
}
is_firewallfalcon_orphan_user() {
local username="$1"
local passwd_line system_user _ uid _ home shell
passwd_line=$(getent passwd "$username" 2>/dev/null) || return 1
IFS=: read -r system_user _ uid _ _ home shell <<< "$passwd_line"
[[ "$uid" =~ ^[0-9]+$ ]] || return 1
db_has_user "$username" && return 1
if id -nG "$username" 2>/dev/null | tr ' ' '\n' | grep -Fxq "$FF_USERS_GROUP"; then
return 0
fi
(( uid >= 1000 )) || return 1
[[ "$home" == "/home/$username" || "$home" == /home/* ]] || return 1
case "$shell" in
/usr/sbin/nologin|/usr/bin/false|/bin/false) return 0 ;;
esac
return 1
}
get_firewallfalcon_orphan_users() {
local username
while IFS=: read -r username _rest; do
[[ -n "$username" ]] || continue
if is_firewallfalcon_orphan_user "$username"; then
echo "$username"
fi
done < /etc/passwd
}
get_firewallfalcon_known_users() {
local username
local -A seen_users=()
if [[ -f "$DB_FILE" ]]; then
while IFS=: read -r username _rest; do
[[ -n "$username" && "$username" != \#* ]] || continue
seen_users["$username"]=1
done < "$DB_FILE"
fi
while IFS= read -r username; do
[[ -n "$username" ]] && seen_users["$username"]=1
done < <(get_firewallfalcon_orphan_users)
(( ${#seen_users[@]} > 0 )) || return 0
printf "%s\n" "${!seen_users[@]}" | sort
}
delete_firewallfalcon_user_accounts() {
local -a users_to_delete=("$@")
local username
[[ ${#users_to_delete[@]} -gt 0 ]] || return 0
for username in "${users_to_delete[@]}"; do
[[ -n "$username" ]] || continue
killall -u "$username" -9 &>/dev/null
pkill -9 -u "$username" &>/dev/null
sleep 0.5
if id "$username" &>/dev/null; then
if userdel -rf "$username" &>/dev/null; then
echo -e " ✅ System user '${C_YELLOW}$username${C_RESET}' deleted."
else
# Retry after harder kill
pkill -9 -u "$username" &>/dev/null
sleep 1
if userdel -rf "$username" &>/dev/null; then
echo -e " ✅ System user '${C_YELLOW}$username${C_RESET}' deleted (retry)."
else
echo -e " ❌ Failed to delete system user '${C_YELLOW}$username${C_RESET}'."
fi
fi
else
echo -e " ℹ️ System user '${C_YELLOW}$username${C_RESET}' was already missing. Removing manager data only."
fi
rm -f "$BANDWIDTH_DIR/${username}.usage"
rm -f "$BANDWIDTH_DIR/${username}.daily_usage"
rm -f "$BANDWIDTH_DIR/${username}.conn_locked"
rm -f "$BANDWIDTH_DIR/${username}.daily_locked"
rm -rf "$BANDWIDTH_DIR/pidtrack/${username}"
done
if [[ -f "$DB_FILE" ]]; then
local db_tmp
db_tmp=$(mktemp)
awk -F: 'NR==FNR { drop[$1]=1; next } !($1 in drop)' <(printf "%s\n" "${users_to_delete[@]}") "$DB_FILE" > "$db_tmp" && mv "$db_tmp" "$DB_FILE"
rm -f "$db_tmp" 2>/dev/null
fi
invalidate_banner_cache
refresh_dynamic_banner_routing_if_enabled
}
require_interactive_terminal() {
if [[ ! -t 0 || ! -t 1 ]]; then
echo -e "${C_RED}❌ Error: The FirewallFalcon menu must be run from an interactive terminal.${C_RESET}"
exit 1
fi
}
initial_setup() {
echo -e "${C_BLUE}⚙️ Initializing TNS243-GLOBAL Manager setup...${C_RESET}"
check_environment
ensure_firewallfalcon_dirs
ensure_firewallfalcon_system_group
echo -e "${C_BLUE}🔹 Configuring user limiter service...${C_RESET}"
setup_limiter_service
echo -e "${C_BLUE}🔹 Configuring bandwidth monitoring service...${C_RESET}"
setup_bandwidth_service
echo -e "${C_BLUE}🔹 Installing trial account cleanup script...${C_RESET}"
setup_trial_cleanup_script
echo -e "${C_BLUE}🔹 Cleaning legacy dynamic SSH banner hooks...${C_RESET}"
disable_dynamic_ssh_banner_system
systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true
if [ ! -f "$INSTALL_FLAG_FILE" ]; then
touch "$INSTALL_FLAG_FILE"
fi
echo -e "${C_GREEN}✅ Setup finished.${C_RESET}"
}
_is_valid_ipv4() {
local ip=$1
if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
return 0
else
return 1
fi
}
check_and_open_firewall_port() {
local port="$1"
local protocol="${2:-tcp}"
local firewall_detected=false
if command -v ufw &> /dev/null && ufw status | grep -q "Status: active"; then
firewall_detected=true
if ! ufw status | grep -qw "$port/$protocol"; then
echo -e "${C_YELLOW}🔥 UFW firewall is active and port ${port}/${protocol} is closed.${C_RESET}"
read -p "👉 Do you want to open this port now? (y/n): " confirm
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
ufw allow "$port/$protocol"
echo -e "${C_GREEN}✅ Port ${port}/${protocol} has been opened in UFW.${C_RESET}"
else
echo -e "${C_RED}❌ Warning: Port ${port}/${protocol} was not opened. The service may not work correctly.${C_RESET}"
return 1
fi
else
echo -e "${C_GREEN}✅ Port ${port}/${protocol} is already open in UFW.${C_RESET}"
fi
fi
if command -v firewall-cmd &> /dev/null && systemctl is-active --quiet firewalld; then
firewall_detected=true
if ! firewall-cmd --list-ports --permanent | grep -qw "$port/$protocol"; then
echo -e "${C_YELLOW}🔥 firewalld is active and port ${port}/${protocol} is not open.${C_RESET}"
read -p "👉 Do you want to open this port now? (y/n): " confirm
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
firewall-cmd --add-port="$port/$protocol" --permanent
firewall-cmd --reload
echo -e "${C_GREEN}✅ Port ${port}/${protocol} has been opened in firewalld.${C_RESET}"
else
echo -e "${C_RED}❌ Warning: Port ${port}/${protocol} was not opened. The service may not work correctly.${C_RESET}"
return 1
fi
else
echo -e "${C_GREEN}✅ Port ${port}/${protocol} is already open in firewalld.${C_RESET}"
fi
fi
if ! $firewall_detected; then
echo -e "${C_BLUE}ℹ️ No active firewall (UFW or firewalld) detected. Assuming ports are open.${C_RESET}"
fi
return 0
}
check_and_open_firewall_port_range() {
local port_range="$1"
local protocol="${2:-tcp}"
local firewall_detected=false
if command -v ufw &> /dev/null && ufw status | grep -q "Status: active"; then
firewall_detected=true
if ! ufw status | grep -Fq "$port_range/$protocol"; then
echo -e "${C_YELLOW}🔥 UFW firewall is active and range ${port_range}/${protocol} is closed.${C_RESET}"
read -p "👉 Do you want to open this port range now? (y/n): " confirm
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
ufw allow "$port_range/$protocol"
echo -e "${C_GREEN}✅ Range ${port_range}/${protocol} has been opened in UFW.${C_RESET}"
else
echo -e "${C_RED}❌ Warning: Range ${port_range}/${protocol} was not opened. The service may not work correctly.${C_RESET}"
return 1
fi
else
echo -e "${C_GREEN}✅ Range ${port_range}/${protocol} is already open in UFW.${C_RESET}"
fi
fi
if command -v firewall-cmd &> /dev/null && systemctl is-active --quiet firewalld; then
firewall_detected=true
if ! firewall-cmd --quiet --query-port="$port_range/$protocol"; then
echo -e "${C_YELLOW}🔥 firewalld is active and range ${port_range}/${protocol} is not open.${C_RESET}"
read -p "👉 Do you want to open this port range now? (y/n): " confirm
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
firewall-cmd --add-port="$port_range/$protocol" --permanent
firewall-cmd --reload
echo -e "${C_GREEN}✅ Range ${port_range}/${protocol} has been opened in firewalld.${C_RESET}"
else
echo -e "${C_RED}❌ Warning: Range ${port_range}/${protocol} was not opened. The service may not work correctly.${C_RESET}"
return 1
fi
else
echo -e "${C_GREEN}✅ Range ${port_range}/${protocol} is already open in firewalld.${C_RESET}"
fi
fi
if ! $firewall_detected; then
echo -e "${C_BLUE}ℹ️ No active firewall (UFW or firewalld) detected. Assuming range ${port_range}/${protocol} is open.${C_RESET}"
fi
return 0
}
check_and_free_ports() {
local ports_to_check=("$@")
for port in "${ports_to_check[@]}"; do
echo -e "\n${C_BLUE}🔎 Checking if port $port is available...${C_RESET}"
local conflicting_process_info
conflicting_process_info=$(
ss -H -lntp "( sport = :$port )" 2>/dev/null
ss -H -lunp "( sport = :$port )" 2>/dev/null
)
if [[ -n "$conflicting_process_info" ]]; then
local conflicting_pid
conflicting_pid=$(echo "$conflicting_process_info" | grep -oP 'pid=\K[0-9]+' | head -n 1)
local conflicting_name
conflicting_name=$(echo "$conflicting_process_info" | grep -oP 'users:\(\("(\K[^"]+)' | head -n 1)
echo -e "${C_YELLOW}⚠️ Warning: Port $port is in use by process '${conflicting_name:-unknown}' (PID: ${conflicting_pid:-N/A}).${C_RESET}"
read -p "👉 Do you want to attempt to stop this process? (y/n): " kill_confirm
if [[ "$kill_confirm" == "y" || "$kill_confirm" == "Y" ]]; then
if [[ -z "$conflicting_pid" ]]; then
echo -e "${C_RED}❌ Could not determine which PID owns port $port. Please free it manually.${C_RESET}"
return 1
fi
echo -e "${C_GREEN}🛑 Stopping process PID $conflicting_pid...${C_RESET}"
systemctl stop "$(ps -p "$conflicting_pid" -o comm=)" &>/dev/null || kill -9 "$conflicting_pid"
sleep 2
if ss -H -lntp "( sport = :$port )" 2>/dev/null | grep -q . || ss -H -lunp "( sport = :$port )" 2>/dev/null | grep -q .; then
echo -e "${C_RED}❌ Failed to free port $port. Please handle it manually. Aborting.${C_RESET}"
return 1
else
echo -e "${C_GREEN}✅ Port $port has been successfully freed.${C_RESET}"
fi
else
echo -e "${C_RED}❌ Cannot proceed without freeing port $port. Aborting.${C_RESET}"
return 1
fi
else
echo -e "${C_GREEN}✅ Port $port is free to use.${C_RESET}"
fi
done
return 0
}
setup_limiter_service() {
# Combined limiter + bandwidth monitoring
cat > "$LIMITER_SCRIPT" << 'EOF'
#!/bin/bash
# FirewallFalcon limiter version 2026-07-23.4
DB_FILE="/etc/firewallfalcon/users.db"
BW_DIR="/etc/firewallfalcon/bandwidth"
PID_DIR="$BW_DIR/pidtrack"
BANNER_DIR="/etc/firewallfalcon/banners"
SCAN_INTERVAL=10
CONN_LOCK_DURATION=180
mkdir -p "$BW_DIR" "$PID_DIR"
shopt -s nullglob
write_banner_if_changed() {
local user="$1"
local content="$2"
local banner_file="$BANNER_DIR/${user}.txt"
local tmp_file="${banner_file}.tmp"
printf "%s" "$content" > "$tmp_file"
if ! cmp -s "$tmp_file" "$banner_file" 2>/dev/null; then
mv "$tmp_file" "$banner_file"
else
rm -f "$tmp_file"
fi
}
# Excess sessions are killed immediately every scan cycle. No account locking.
while true; do
if [[ ! -s "$DB_FILE" ]]; then
sleep "$SCAN_INTERVAL"
continue
fi
# Daily reset logic
today=$(date +%Y-%m-%d)
if [[ ! -f "$BW_DIR/current_date" ]]; then
echo "$today" > "$BW_DIR/current_date"
fi
saved_date=$(cat "$BW_DIR/current_date" 2>/dev/null || echo "$today")
if [[ "$today" != "$saved_date" ]]; then
# New day! Reset daily usage and unlock users locked due to daily limit
rm -f "$BW_DIR/"*.daily_usage 2>/dev/null
for locked_file in "$BW_DIR/"*.daily_locked; do
[[ -f "$locked_file" ]] || continue
locked_user=$(basename "$locked_file" .daily_locked)
usermod -U "$locked_user" &>/dev/null
rm -f "$locked_file"
done
echo "$today" > "$BW_DIR/current_date"
fi
# Connection limit auto-unlock: check marker files and unlock after CONN_LOCK_DURATION seconds
for conn_lock_file in "$BW_DIR/"*.conn_locked; do
[[ -f "$conn_lock_file" ]] || continue
lock_ts=0
read -r lock_ts < "$conn_lock_file" 2>/dev/null || lock_ts=0
[[ "$lock_ts" =~ ^[0-9]+$ ]] || lock_ts=0
printf -v now_ts '%(%s)T' -1
if (( now_ts - lock_ts >= CONN_LOCK_DURATION )); then
conn_locked_user=$(basename "$conn_lock_file" .conn_locked)
usermod -U "$conn_locked_user" &>/dev/null
rm -f "$conn_lock_file"
fi
done
printf -v current_ts '%(%s)T' -1
# Backup trial cleanup: check .trial_expiry files for expired trials
for trial_file in "$BW_DIR/"*.trial_expiry; do
[[ -f "$trial_file" ]] || continue
trial_exp_ts=0
read -r trial_exp_ts < "$trial_file" 2>/dev/null || trial_exp_ts=0
[[ "$trial_exp_ts" =~ ^[0-9]+$ ]] || trial_exp_ts=0
if (( trial_exp_ts > 0 && current_ts >= trial_exp_ts )); then
trial_user=$(basename "$trial_file" .trial_expiry)
# Run the cleanup script if it exists, otherwise do inline cleanup
if [[ -x "$TRIAL_CLEANUP_SCRIPT" ]]; then
"$TRIAL_CLEANUP_SCRIPT" "$trial_user" &>/dev/null
else
killall -u "$trial_user" -9 &>/dev/null
pkill -9 -u "$trial_user" &>/dev/null
userdel -rf "$trial_user" &>/dev/null
sed -i "/^${trial_user}:/d" "$DB_FILE"
rm -f "$BW_DIR/${trial_user}.usage" "$BW_DIR/${trial_user}.daily_usage"
rm -rf "$BW_DIR/pidtrack/${trial_user}"
fi
rm -f "$trial_file"
fi
done
dynamic_banners_enabled=false
# Reset associative arrays each cycle (unset first to avoid stale data)
unset session_pids locked_users uid_to_user loginuid_pids
declare -A session_pids=()
declare -A locked_users=()
declare -A uid_to_user=()
declare -A loginuid_pids=()
while IFS=: read -r username _ uid _rest; do
[[ -n "$username" && "$uid" =~ ^[0-9]+$ ]] && uid_to_user["$uid"]="$username"
done < /etc/passwd
# Method 1: process owner from ps (primary source for connection counting)
# sshd-session is the user-owned process on Ubuntu 24.04+ (OpenSSH 9.8+)
# On Ubuntu 22, the per-session sshd is user-owned instead.
# Either way, exactly 1 user-owned process exists per SSH session.
while read -r ssh_pid ssh_owner; do
[[ "$ssh_pid" =~ ^[0-9]+$ ]] || continue
if [[ -n "$ssh_owner" && "$ssh_owner" != "root" && "$ssh_owner" != "sshd" ]]; then
session_pids["$ssh_owner"]+="$ssh_pid "
fi
done < <(ps -C sshd,sshd-session -o pid=,user= 2>/dev/null)
# Method 2: kernel loginuid (reliable even when sshd runs as root)
for p in /proc/[0-9]*/loginuid; do
[[ -f "$p" ]] || continue
login_uid=""
read -r login_uid < "$p" || login_uid=""
[[ "$login_uid" =~ ^[0-9]+$ && "$login_uid" != "4294967295" ]] || continue
session_user="${uid_to_user[$login_uid]}"
[[ -n "$session_user" ]] || continue
pid_dir=$(dirname "$p")
pid_num=$(basename "$pid_dir")
comm=""
read -r comm < "$pid_dir/comm" || comm=""
[[ "$comm" == "sshd" ]] || continue
ppid_val=""
while read -r key value; do
if [[ "$key" == "PPid:" ]]; then
ppid_val="${value:-}"
break
fi
done < "$pid_dir/status"
[[ "$ppid_val" == "1" ]] && continue
loginuid_pids["$session_user"]+="$pid_num "
done
# Detect locked users via /etc/shadow (cheaper than passwd -Sa)
if [[ -r /etc/shadow ]]; then
while IFS=: read -r shadow_user shadow_hash _rest; do
[[ -n "$shadow_user" && "${shadow_hash:0:1}" == "!" ]] && locked_users["$shadow_user"]=1
done < /etc/shadow
else
while read -r passwd_user _ passwd_status _rest; do
[[ "$passwd_status" == "L" ]] && locked_users["$passwd_user"]=1
done < <(passwd -Sa 2>/dev/null)
fi
if [[ -f "/etc/firewallfalcon/banners_enabled" ]]; then
mkdir -p "$BANNER_DIR"
dynamic_banners_enabled=true
fi
while IFS=: read -r user pass expiry limit bandwidth_gb daily_bandwidth_gb _extra; do
[[ -z "$user" || "$user" == \#* ]] && continue
[[ ! "$daily_bandwidth_gb" =~ ^[0-9]+\.?[0-9]*$ ]] && daily_bandwidth_gb=0
# CRITICAL: unset before declare to reset per-user (bash declare is function-scoped)
unset unique_pids
declare -A unique_pids=()
# Use ONLY ps-based session_pids for connection counting.
# loginuid_pids can double-count (root-owned sshd has user's loginuid on Ubuntu 24)
for pid in ${session_pids[$user]}; do
[[ "$pid" =~ ^[0-9]+$ ]] && unique_pids["$pid"]=1
done
online_count=${#unique_pids[@]}
user_locked=false
if [[ -n "${locked_users[$user]+x}" ]]; then
user_locked=true
fi
expiry_ts=0
if [[ "$expiry" != "Never" && -n "$expiry" && "$expiry" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
expiry_ts=$(date -d "$expiry" +%s 2>/dev/null || echo 0)
if [[ "$expiry_ts" =~ ^[0-9]+$ ]] && (( expiry_ts > 0 && expiry_ts < current_ts )); then
if ! $user_locked; then
usermod -L "$user" &>/dev/null
killall -u "$user" -9 &>/dev/null
locked_users["$user"]=1
fi
continue
fi
fi
[[ "$limit" =~ ^[0-9]+$ ]] || limit=1
if (( online_count > limit )); then
if ! $user_locked; then
usermod -L "$user" &>/dev/null
killall -u "$user" -9 &>/dev/null
locked_users["$user"]=1
user_locked=true
printf -v now_ts '%(%s)T' -1
echo "$now_ts" > "$BW_DIR/${user}.conn_locked"
fi
continue
fi
if $dynamic_banners_enabled; then
days_left="N/A"
if [[ "$expiry" != "Never" && -n "$expiry" && "$expiry_ts" =~ ^[0-9]+$ && $expiry_ts -gt 0 ]]; then
diff_secs=$((expiry_ts - current_ts))
if (( diff_secs <= 0 )); then
days_left="EXPIRED"
else
d_l=$(( diff_secs / 86400 ))
h_l=$(( (diff_secs % 86400) / 3600 ))
if (( d_l == 0 )); then
days_left="${h_l}h left"
else
days_left="${d_l}d ${h_l}h"
fi
fi
fi
bw_info="Unlimited"
if [[ "$bandwidth_gb" != "0" && -n "$bandwidth_gb" ]]; then
usagefile="$BW_DIR/${user}.usage"
accum_disp=0
if [[ -f "$usagefile" ]]; then
read -r accum_disp < "$usagefile"
[[ "$accum_disp" =~ ^[0-9]+$ ]] || accum_disp=0
fi
used_gb_int=$((accum_disp / 1073741824))
used_gb_frac=$(( (accum_disp % 1073741824) * 100 / 1073741824 ))
printf -v used_gb "%d.%02d" "$used_gb_int" "$used_gb_frac"
quota_b=$(( ${bandwidth_gb%%.*} * 1073741824 ))
remain_b=$(( quota_b - accum_disp ))
(( remain_b < 0 )) && remain_b=0
remain_gb_int=$((remain_b / 1073741824))
remain_gb_frac=$(( (remain_b % 1073741824) * 100 / 1073741824 ))
printf -v remain_gb "%d.%02d" "$remain_gb_int" "$remain_gb_frac"
bw_info="${used_gb}/${bandwidth_gb} GB used | ${remain_gb} GB left"
fi
banner_content="
✨ ACCOUNT STATUS ✨
"
banner_content+="👤 Username : $user
"
banner_content+="📅 Expiration : $expiry ($days_left)
"
if [[ "$bandwidth_gb" != "0" ]]; then
banner_content+="📊 Total BW : $bw_info
"
fi
if [[ "$daily_bandwidth_gb" != "0" ]]; then
daily_usagefile="$BW_DIR/${user}.daily_usage"
accum_disp=0
if [[ -f "$daily_usagefile" ]]; then
read -r accum_disp < "$daily_usagefile"
[[ "$accum_disp" =~ ^[0-9]+$ ]] || accum_disp=0
fi
used_gb_int=$((accum_disp / 1073741824))
used_gb_frac=$(( (accum_disp % 1073741824) * 100 / 1073741824 ))
printf -v used_gb "%d.%02d" "$used_gb_int" "$used_gb_frac"
quota_b=$(( ${daily_bandwidth_gb%%.*} * 1073741824 ))
remain_b=$(( quota_b - accum_disp ))
(( remain_b < 0 )) && remain_b=0
remain_gb_int=$((remain_b / 1073741824))
remain_gb_frac=$(( (remain_b % 1073741824) * 100 / 1073741824 ))
printf -v remain_gb "%d.%02d" "$remain_gb_int" "$remain_gb_frac"
daily_bw_info="${used_gb}/${daily_bandwidth_gb} GB used | ${remain_gb} GB left"
banner_content+="📊 Daily BW : $daily_bw_info
"
fi
banner_content+="🔌 Sessions : $online_count/$limit
"
write_banner_if_changed "$user" "$banner_content"
fi
[[ ( -z "$bandwidth_gb" || "$bandwidth_gb" == "0" ) && ( -z "$daily_bandwidth_gb" || "$daily_bandwidth_gb" == "0" ) ]] && continue
usagefile="$BW_DIR/${user}.usage"
accumulated=0
if [[ -f "$usagefile" ]]; then
read -r accumulated < "$usagefile"
[[ "$accumulated" =~ ^[0-9]+$ ]] || accumulated=0
fi
if (( ${#unique_pids[@]} == 0 )); then
rm -f "$PID_DIR/${user}__"*.last 2>/dev/null
continue
fi
delta_total=0
for pid in "${!unique_pids[@]}"; do
io_file="/proc/$pid/io"
cur=0
if [[ -r "$io_file" ]]; then
rchar=0
wchar=0
while read -r key value; do
case "$key" in
rchar:) rchar=${value:-0} ;;
wchar:) wchar=${value:-0} ;;
esac
done < "$io_file"
cur=$((rchar + wchar))
fi
pidfile="$PID_DIR/${user}__${pid}.last"
if [[ -f "$pidfile" ]]; then
read -r prev < "$pidfile"
[[ "$prev" =~ ^[0-9]+$ ]] || prev=0
if (( cur >= prev )); then
d=$((cur - prev))
else
d=$cur
fi
delta_total=$((delta_total + d))
fi
printf "%s\n" "$cur" > "$pidfile"
done
for f in "$PID_DIR/${user}__"*.last; do
[[ -f "$f" ]] || continue
fpid=${f##*__}
fpid=${fpid%.last}
[[ -d "/proc/$fpid" ]] || rm -f "$f"
done
new_total=$((accumulated + delta_total))
printf "%s\n" "$new_total" > "$usagefile"
if awk "BEGIN{exit(!($bandwidth_gb > 0))}" 2>/dev/null; then
quota_bytes=$(awk "BEGIN{printf \"%.0f\", $bandwidth_gb * 1073741824}")
if [[ "$quota_bytes" =~ ^[0-9]+$ ]] && (( quota_bytes > 0 && new_total >= quota_bytes )); then
if ! $user_locked; then
usermod -L "$user" &>/dev/null
killall -u "$user" -9 &>/dev/null
locked_users["$user"]=1
user_locked=true
fi
fi
fi
daily_usagefile="$BW_DIR/${user}.daily_usage"
daily_accumulated=0
if [[ -f "$daily_usagefile" ]]; then
read -r daily_accumulated < "$daily_usagefile"
[[ "$daily_accumulated" =~ ^[0-9]+$ ]] || daily_accumulated=0
fi
new_daily_total=$((daily_accumulated + delta_total))
printf "%s\n" "$new_daily_total" > "$daily_usagefile"
if awk "BEGIN{exit(!($daily_bandwidth_gb > 0))}" 2>/dev/null; then
d_quota_bytes=$(awk "BEGIN{printf \"%.0f\", $daily_bandwidth_gb * 1073741824}")
if [[ "$d_quota_bytes" =~ ^[0-9]+$ ]] && (( d_quota_bytes > 0 && new_daily_total >= d_quota_bytes )); then
if ! $user_locked; then
usermod -L "$user" &>/dev/null
killall -u "$user" -9 &>/dev/null
locked_users["$user"]=1
user_locked=true
touch "$BW_DIR/${user}.daily_locked"
fi
fi
fi
done < "$DB_FILE"
sleep "$SCAN_INTERVAL"
done
EOF
chmod +x "$LIMITER_SCRIPT"
# Strip DOS line endings in case menu.sh was uploaded from Windows
sed -i 's/\r$//' "$LIMITER_SCRIPT" 2>/dev/null
cat > "$LIMITER_SERVICE" << EOF
[Unit]
Description=FirewallFalcon Active User Limiter
After=network.target
[Service]
Type=simple
ExecStart=$LIMITER_SCRIPT
Restart=always
RestartSec=10
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
MemoryHigh=48M
MemoryMax=64M
[Install]
WantedBy=multi-user.target
EOF
sed -i 's/\r$//' "$LIMITER_SERVICE" 2>/dev/null
pkill -f "firewallfalcon-limiter" 2>/dev/null
if ! systemctl is-active --quiet firewallfalcon-limiter; then
systemctl daemon-reload
systemctl enable firewallfalcon-limiter &>/dev/null
systemctl start firewallfalcon-limiter --no-block &>/dev/null
else
systemctl restart firewallfalcon-limiter --no-block &>/dev/null
fi
}
sync_runtime_components_if_needed() {
local limiter_marker="# FirewallFalcon limiter version 2026-07-23.8"
cleanup_legacy_bandwidth_runtime
setup_trial_cleanup_script >/dev/null 2>&1
if [[ ! -f "$LIMITER_SCRIPT" ]] || ! grep -Fqx "$limiter_marker" "$LIMITER_SCRIPT" 2>/dev/null; then
setup_limiter_service >/dev/null 2>&1
fi
if [[ -f "$BADVPN_SERVICE_FILE" ]]; then
ensure_badvpn_service_is_quiet
fi
if [[ -f "/etc/firewallfalcon/banners_enabled" ]]; then
update_ssh_banners_config
elif [[ -f "$SSHD_FF_CONFIG" ]]; then
disable_dynamic_ssh_banner_system
systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true
fi
}
setup_bandwidth_service() {
mkdir -p "$BANDWIDTH_DIR"
# Bandwidth monitoring is now integrated into the limiter service above.
cleanup_legacy_bandwidth_runtime
}
cleanup_legacy_bandwidth_runtime() {
local needs_reload=false
systemctl stop firewallfalcon-bandwidth &>/dev/null || true
systemctl disable firewallfalcon-bandwidth &>/dev/null || true
pkill -f "firewallfalcon-bandwidth" &>/dev/null || true
if [[ -e "$BANDWIDTH_SERVICE" || -e "$BANDWIDTH_SCRIPT" || -e "$LEGACY_BANDWIDTH_DIR" ]]; then
rm -f "$BANDWIDTH_SERVICE" "$BANDWIDTH_SCRIPT" 2>/dev/null
rm -rf "$LEGACY_BANDWIDTH_DIR" 2>/dev/null
needs_reload=true
fi
if $needs_reload; then
systemctl daemon-reload &>/dev/null || true
fi
}
setup_trial_cleanup_script() {
cat > "$TRIAL_CLEANUP_SCRIPT" << 'TREOF'
#!/bin/bash
# FirewallFalcon Trial Account Auto-Cleanup
# Usage: firewallfalcon-trial-cleanup.sh
DB_FILE="/etc/firewallfalcon/users.db"
BW_DIR="/etc/firewallfalcon/bandwidth"
username="$1"
if [[ -z "$username" ]]; then exit 1; fi
db_line=$(grep "^${username}:" "$DB_FILE" 2>/dev/null | head -n 1)
if [[ -z "$db_line" ]]; then exit 0; fi
IFS=: read -r _ _ _ _ _ _ trial_marker _rest <<< "$db_line"
if [[ "$trial_marker" != "trial" ]]; then
exit 0
fi
# Kill active sessions
killall -u "$username" -9 &>/dev/null
pkill -9 -u "$username" &>/dev/null
sleep 1
# Delete system user
userdel -rf "$username" &>/dev/null
# Remove from DB
sed -i "/^${username}:/d" "$DB_FILE"
# Remove bandwidth tracking and trial expiry marker
rm -f "$BW_DIR/${username}.usage" "$BW_DIR/${username}.daily_usage" "$BW_DIR/${username}.trial_expiry"
rm -rf "$BW_DIR/pidtrack/${username}"
TREOF
chmod +x "$TRIAL_CLEANUP_SCRIPT"
}
disable_dynamic_ssh_banner_system() {
rm -f "/etc/firewallfalcon/banners_enabled" "$SSHD_FF_CONFIG" /usr/local/bin/firewallfalcon-login-info.sh 2>/dev/null
rm -rf "/etc/firewallfalcon/banners" 2>/dev/null
invalidate_banner_cache
}
disable_static_ssh_banner_in_sshd_config() {
sed -i.bak -E "s|^[[:space:]]*Banner[[:space:]]+$SSH_BANNER_FILE[[:space:]]*$|# Banner $SSH_BANNER_FILE|" /etc/ssh/sshd_config 2>/dev/null
}
is_static_ssh_banner_enabled() {
grep -q -E "^[[:space:]]*Banner[[:space:]]+$SSH_BANNER_FILE[[:space:]]*$" /etc/ssh/sshd_config 2>/dev/null && [ -f "$SSH_BANNER_FILE" ]
}
is_dynamic_ssh_banner_enabled() {
[[ -f "/etc/firewallfalcon/banners_enabled" && -f "$SSHD_FF_CONFIG" ]]
}
get_ssh_banner_mode() {
if is_dynamic_ssh_banner_enabled; then
echo "dynamic"
elif is_static_ssh_banner_enabled; then
echo "static"
else
echo "disabled"
fi
}
refresh_dynamic_banner_routing_if_enabled() {
if is_dynamic_ssh_banner_enabled; then
update_ssh_banners_config
fi
}
update_ssh_banners_config() {
local tmp_conf
if [[ ! -f "/etc/firewallfalcon/banners_enabled" ]]; then
if [[ -f "$SSHD_FF_CONFIG" ]]; then
rm -f "$SSHD_FF_CONFIG" 2>/dev/null
systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null
fi
return
fi
ensure_firewallfalcon_dirs
tmp_conf="/tmp/ff_banners_new.conf"
echo "# FirewallFalcon - Dynamic per-user SSH banners" > "$tmp_conf"
if [[ -f "$DB_FILE" ]]; then
while IFS=: read -r u _rest; do
[[ -z "$u" || "$u" == \#* ]] && continue
echo "Match User $u" >> "$tmp_conf"
echo " Banner /etc/firewallfalcon/banners/${u}.txt" >> "$tmp_conf"
done < "$DB_FILE"
fi
if ! cmp -s "$tmp_conf" "$SSHD_FF_CONFIG" 2>/dev/null; then
mv "$tmp_conf" "$SSHD_FF_CONFIG"
if ! grep -q "^Include /etc/ssh/sshd_config.d/" /etc/ssh/sshd_config 2>/dev/null; then
echo "Include /etc/ssh/sshd_config.d/*.conf" >> /etc/ssh/sshd_config
fi
systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null
else
rm -f "$tmp_conf"
fi
}
setup_ssh_login_info() {
ensure_firewallfalcon_dirs || return 1
if ! touch "/etc/firewallfalcon/banners_enabled"; then
echo -e "${C_RED}❌ Failed to enable dynamic SSH banners.${C_RESET}"
return 1
fi
disable_static_ssh_banner_in_sshd_config
update_ssh_banners_config
return 0
}
generate_dns_record() {
echo -e "\n${C_BLUE}⚙️ Generating a random domain...${C_RESET}"
if ! command -v jq &> /dev/null; then
echo -e "${C_YELLOW}⚠️ jq not found, attempting to install...${C_RESET}"
ff_pkg_install jq >/dev/null 2>&1 || {
echo -e "${C_RED}❌ Failed to install jq. Cannot manage DNS records.${C_RESET}"
return 1
}
fi
local SERVER_IPV4
SERVER_IPV4=$(curl -s -4 icanhazip.com)
if ! _is_valid_ipv4 "$SERVER_IPV4"; then
echo -e "\n${C_RED}❌ Error: Could not retrieve a valid public IPv4 address from icanhazip.com.${C_RESET}"
echo -e "${C_YELLOW}ℹ️ Please check your server's network connection and DNS resolver settings.${C_RESET}"
echo -e " Output received: '$SERVER_IPV4'"
return 1
fi
local SERVER_IPV6
SERVER_IPV6=$(curl -s -6 icanhazip.com --max-time 5)
local RANDOM_SUBDOMAIN="vps-$(tr -dc a-z0-9 < /dev/urandom | head -c 8)"
local FULL_DOMAIN="$RANDOM_SUBDOMAIN.$DESEC_DOMAIN"
local HAS_IPV6="false"
local API_DATA
API_DATA=$(printf '[{"subname": "%s", "type": "A", "ttl": 3600, "records": ["%s"]}]' "$RANDOM_SUBDOMAIN" "$SERVER_IPV4")
if [[ -n "$SERVER_IPV6" ]]; then
local aaaa_record
aaaa_record=$(printf ',{"subname": "%s", "type": "AAAA", "ttl": 3600, "records": ["%s"]}' "$RANDOM_SUBDOMAIN" "$SERVER_IPV6")
API_DATA="${API_DATA%?}${aaaa_record}]"
HAS_IPV6="true"
fi
local CREATE_RESPONSE
CREATE_RESPONSE=$(curl -s -w "%{http_code}" -X POST "https://desec.io/api/v1/domains/$DESEC_DOMAIN/rrsets/" \
-H "Authorization: Token $DESEC_TOKEN" -H "Content-Type: application/json" \
--data "$API_DATA")
local HTTP_CODE=${CREATE_RESPONSE: -3}
local RESPONSE_BODY=${CREATE_RESPONSE:0:${#CREATE_RESPONSE}-3}
if [[ "$HTTP_CODE" -ne 201 ]]; then
echo -e "${C_RED}❌ Failed to create DNS records. API returned HTTP $HTTP_CODE.${C_RESET}"
if ! echo "$RESPONSE_BODY" | jq . > /dev/null 2>&1; then
echo "Raw Response: $RESPONSE_BODY"
else
echo "Response: $RESPONSE_BODY" | jq
fi
return 1
fi
cat > "$DNS_INFO_FILE" <<-EOF
SUBDOMAIN="$RANDOM_SUBDOMAIN"
FULL_DOMAIN="$FULL_DOMAIN"
HAS_IPV6="$HAS_IPV6"
EOF
echo -e "\n${C_GREEN}✅ Successfully created domain: ${C_YELLOW}$FULL_DOMAIN${C_RESET}"
}
delete_dns_record() {
if [ ! -f "$DNS_INFO_FILE" ]; then
echo -e "\n${C_YELLOW}ℹ️ No domain to delete.${C_RESET}"
return
fi
echo -e "\n${C_BLUE}🗑️ Deleting DNS records...${C_RESET}"
source "$DNS_INFO_FILE"
if [[ -z "$SUBDOMAIN" ]]; then
echo -e "${C_RED}❌ Could not read record details from config file. Skipping deletion.${C_RESET}"
return
fi
curl -s -X DELETE "https://desec.io/api/v1/domains/$DESEC_DOMAIN/rrsets/$SUBDOMAIN/A/" \
-H "Authorization: Token $DESEC_TOKEN" > /dev/null
if [[ "$HAS_IPV6" == "true" ]]; then
curl -s -X DELETE "https://desec.io/api/v1/domains/$DESEC_DOMAIN/rrsets/$SUBDOMAIN/AAAA/" \
-H "Authorization: Token $DESEC_TOKEN" > /dev/null
fi
echo -e "\n${C_GREEN}✅ Deleted domain: ${C_YELLOW}$FULL_DOMAIN${C_RESET}"
rm -f "$DNS_INFO_FILE"
}
dns_menu() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🌐 DNS Domain Management ---${C_RESET}"
if [ -f "$DNS_INFO_FILE" ]; then
source "$DNS_INFO_FILE"
echo -e "\nℹ️ A domain already exists for this server:"
echo -e " - ${C_CYAN}Domain:${C_RESET} ${C_YELLOW}$FULL_DOMAIN${C_RESET}"
echo
read -p "👉 Do you want to DELETE this domain? (y/n): " choice
if [[ "$choice" == "y" || "$choice" == "Y" ]]; then
delete_dns_record
else
echo -e "\n${C_YELLOW}❌ Action cancelled.${C_RESET}"
fi
else
echo -e "\nℹ️ No domain has been generated for this server yet."
echo
read -p "👉 Do you want to generate a new random domain now? (y/n): " choice
if [[ "$choice" == "y" || "$choice" == "Y" ]]; then
generate_dns_record
else
echo -e "\n${C_YELLOW}❌ Action cancelled.${C_RESET}"
fi
fi
}
_select_user_interface() {
local title="$1"
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}${title}${C_RESET}\n"
if [[ ! -s $DB_FILE ]]; then
echo -e "${C_YELLOW}ℹ️ No users found in the database.${C_RESET}"
SELECTED_USER="NO_USERS"; return
fi
mapfile -t all_users < <(cut -d: -f1 "$DB_FILE" | sort)
local -A all_user_lookup=()
local username
for username in "${all_users[@]}"; do
all_user_lookup["$username"]=1
done
if [ ${#all_users[@]} -ge 15 ]; then
read -p "👉 Enter a search term (or press Enter to list all): " search_term
if [[ -n "$search_term" ]]; then
mapfile -t users < <(printf "%s\n" "${all_users[@]}" | grep -i "$search_term")
else
users=("${all_users[@]}")
fi
else
users=("${all_users[@]}")
fi
if [ ${#users[@]} -eq 0 ]; then
echo -e "\n${C_YELLOW}ℹ️ No users found matching your criteria.${C_RESET}"
SELECTED_USER="NO_USERS"; return
fi
echo -e "\nPlease select a user:\n"
for i in "${!users[@]}"; do
printf " ${C_GREEN}[%2d]${C_RESET} %s\n" "$((i+1))" "${users[$i]}"
done
echo -e "\n ${C_RED} [ 0]${C_RESET} ↩️ Cancel"
echo -e "${C_CYAN}💡 Tip: you can also type the exact username directly.${C_RESET}"
echo
local choice
while true; do
if ! read -r -p "👉 Enter the number or exact username: " choice; then
echo
SELECTED_USER=""
return
fi
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 0 ] && [ "$choice" -le "${#users[@]}" ]; then
if [ "$choice" -eq 0 ]; then
SELECTED_USER=""; return
else
SELECTED_USER="${users[$((choice-1))]}"; return
fi
elif [[ -n "${all_user_lookup[$choice]+x}" ]]; then
SELECTED_USER="$choice"; return
else
echo -e "${C_RED}❌ Invalid selection. Please try again.${C_RESET}"
fi
done
}
_select_multi_user_interface() {
local title="$1"
local include_orphan_users="${2:-false}"
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}${title}${C_RESET}\n"
SELECTED_USERS=()
local -a all_users=()
local -a orphan_users=()
local -A all_user_lookup=()
local -A orphan_user_lookup=()
local username
if [[ -s $DB_FILE ]]; then
mapfile -t all_users < <(cut -d: -f1 "$DB_FILE" | sort)
fi
if [[ "$include_orphan_users" == "true" ]]; then
mapfile -t orphan_users < <(get_firewallfalcon_orphan_users)
for username in "${orphan_users[@]}"; do
orphan_user_lookup["$username"]=1
if ! printf "%s\n" "${all_users[@]}" | grep -Fxq "$username"; then
all_users+=("$username")
fi
done
if [[ ${#all_users[@]} -gt 0 ]]; then
mapfile -t all_users < <(printf "%s\n" "${all_users[@]}" | sort)
fi
fi
if [[ ${#all_users[@]} -eq 0 ]]; then
echo -e "${C_YELLOW}ℹ️ No users found in the manager database.${C_RESET}"
if [[ "$include_orphan_users" == "true" ]]; then
echo -e "${C_DIM}No orphan FirewallFalcon system users were found either.${C_RESET}"
fi
SELECTED_USERS=("NO_USERS"); return
fi
for username in "${all_users[@]}"; do
all_user_lookup["$username"]=1
done
if [ ${#all_users[@]} -ge 15 ]; then
read -p "👉 Enter a search term (or press Enter to list all): " search_term
if [[ -n "$search_term" ]]; then
mapfile -t users < <(printf "%s\n" "${all_users[@]}" | grep -i "$search_term")
else
users=("${all_users[@]}")
fi
else
users=("${all_users[@]}")
fi
if [ ${#users[@]} -eq 0 ]; then
echo -e "\n${C_YELLOW}ℹ️ No users found matching your criteria.${C_RESET}"
SELECTED_USERS=("NO_USERS"); return
fi
echo -e "\nPlease select users:\n"
for i in "${!users[@]}"; do
local display_user="${users[$i]}"
if [[ "$include_orphan_users" == "true" && -n "${orphan_user_lookup[${users[$i]}]+x}" ]]; then
display_user="${display_user} ${C_DIM}(system-only)${C_RESET}"
fi
printf " ${C_GREEN}[%2d]${C_RESET} %s\n" "$((i+1))" "$display_user"
done
echo -e "\n ${C_GREEN}[all]${C_RESET} Select ALL listed users"
echo -e " ${C_RED} [0]${C_RESET} ↩️ Cancel and return to main menu"
echo -e "\n${C_CYAN}💡 You can select multiple by number, range, or exact username.${C_RESET}"
echo -e "${C_CYAN} Examples: '1 3 5' or '1,3' or '1-4' or 'alice bob'${C_RESET}"
if [[ "$include_orphan_users" == "true" ]]; then
echo -e "${C_CYAN} Users marked '(system-only)' are old accounts still on the VPS but missing from users.db${C_RESET}"
fi
echo
local choice
while true; do
if ! read -r -p "👉 Enter user numbers or usernames: " choice; then
echo
SELECTED_USERS=()
return
fi
choice=${choice//,/ } # Replace commas with spaces
if [[ -z "$choice" ]]; then
echo -e "${C_RED}❌ Invalid selection. Please try again.${C_RESET}"
continue
fi
if [[ "$choice" == "0" ]]; then
SELECTED_USERS=(); return
fi
if [[ "${choice,,}" == "all" ]]; then
SELECTED_USERS=("${users[@]}")
return
fi
local valid=true
local selected_indices=()
local selected_names=()
for token in $choice; do
if [[ "$token" =~ ^[0-9]+-[0-9]+$ ]]; then
local start=${token%-*}
local end=${token#*-}
if [ "$start" -le "$end" ]; then
for (( idx=start; idx<=end; idx++ )); do
if [ "$idx" -ge 1 ] && [ "$idx" -le "${#users[@]}" ]; then
selected_indices+=($idx)
else
valid=false; break
fi
done
else
valid=false; break
fi
elif [[ "$token" =~ ^[0-9]+$ ]]; then
if [ "$token" -ge 1 ] && [ "$token" -le "${#users[@]}" ]; then
selected_indices+=($token)
elif [[ -n "${all_user_lookup[$token]+x}" ]]; then
selected_names+=("$token")
else
valid=false; break
fi
elif [[ -n "${all_user_lookup[$token]+x}" ]]; then
selected_names+=("$token")
else
valid=false; break
fi
done
if [[ "$valid" == true && ( ${#selected_indices[@]} -gt 0 || ${#selected_names[@]} -gt 0 ) ]]; then
mapfile -t unique_indices < <(printf "%s\n" "${selected_indices[@]}" | sort -u -n)
for idx in "${unique_indices[@]}"; do
SELECTED_USERS+=("${users[$((idx-1))]}")
done
if (( ${#selected_names[@]} > 0 )); then
mapfile -t unique_names < <(printf "%s\n" "${selected_names[@]}" | sort -u)
for username in "${unique_names[@]}"; do
if [[ -n "$username" ]] && ! printf "%s\n" "${SELECTED_USERS[@]}" | grep -Fxq "$username"; then
SELECTED_USERS+=("$username")
fi
done
fi
return
else
echo -e "${C_RED}❌ Invalid selection. Please check your numbers or usernames.${C_RESET}"
SELECTED_USERS=()
selected_indices=()
selected_names=()
fi
done
}
get_user_status() {
local username="$1"
if ! id "$username" &>/dev/null; then echo -e "${C_RED}Not Found${C_RESET}"; return; fi
local expiry_date=$(grep "^$username:" "$DB_FILE" | cut -d: -f3)
if passwd -S "$username" 2>/dev/null | grep -q " L "; then echo -e "${C_YELLOW}🔒 Locked${C_RESET}"; return; fi
local expiry_ts=$(date -d "$expiry_date" +%s 2>/dev/null || echo 0)
local current_ts=$(date +%s)
if [[ $expiry_ts -lt $current_ts ]]; then echo -e "${C_RED}🗓️ Expired${C_RESET}"; return; fi
echo -e "${C_GREEN}🟢 Active${C_RESET}"
}
create_user() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- ✨ Create New SSH User ---${C_RESET}"
read -p "👉 Enter username (or '0' to cancel): " username
local adopt_existing=false
if [[ "$username" == "0" ]]; then
echo -e "\n${C_YELLOW}❌ User creation cancelled.${C_RESET}"
return
fi
if [[ -z "$username" ]]; then
echo -e "\n${C_RED}❌ Error: Username cannot be empty.${C_RESET}"
return
fi
if db_has_user "$username"; then
echo -e "\n${C_RED}❌ Error: User '$username' already exists in FirewallFalcon.${C_RESET}"
return
fi
if id "$username" &>/dev/null; then
if is_firewallfalcon_orphan_user "$username"; then
echo -e "\n${C_YELLOW}⚠️ User '$username' already exists on the system but is missing from users.db.${C_RESET}"
echo -e "${C_DIM}This usually happens after uninstalling the script without deleting the SSH users.${C_RESET}"
read -p "👉 Do you want to take control of this existing user and manage it with FirewallFalcon? (y/n): " adopt_confirm
if [[ "$adopt_confirm" == "y" || "$adopt_confirm" == "Y" ]]; then
adopt_existing=true
else
echo -e "\n${C_YELLOW}❌ User creation cancelled.${C_RESET}"
return
fi
else
echo -e "\n${C_RED}❌ Error: System user '$username' already exists and does not look like a FirewallFalcon SSH account.${C_RESET}"
return
fi
fi
local password=""
while true; do
read -p "🔑 Enter password (or press Enter for auto-generated): " password
if [[ -z "$password" ]]; then
password=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 8)
echo -e "${C_GREEN}🔑 Auto-generated password: ${C_YELLOW}$password${C_RESET}"
break
else
break
fi
done
read -p "🗓️ Enter account duration (in days) [30]: " days
days=${days:-30}
if ! [[ "$days" =~ ^[0-9]+$ ]]; then echo -e "\n${C_RED}❌ Invalid number.${C_RESET}"; return; fi
read -p "📶 Enter simultaneous connection limit [1]: " limit
limit=${limit:-1}
if ! [[ "$limit" =~ ^[0-9]+$ ]]; then echo -e "\n${C_RED}❌ Invalid number.${C_RESET}"; return; fi
read -p "📦 Enter bandwidth limit in GB (0 = unlimited) [0]: " bandwidth_gb
bandwidth_gb=${bandwidth_gb:-0}
if ! [[ "$bandwidth_gb" =~ ^[0-9]+\.?[0-9]*$ ]]; then echo -e "\n${C_RED}❌ Invalid number.${C_RESET}"; return; fi
read -p "📦 Enter DAILY bandwidth limit in GB (0 = unlimited) [0]: " daily_bandwidth_gb
daily_bandwidth_gb=${daily_bandwidth_gb:-0}
if ! [[ "$daily_bandwidth_gb" =~ ^[0-9]+\.?[0-9]*$ ]]; then echo -e "\n${C_RED}❌ Invalid number.${C_RESET}"; return; fi
local expire_date
expire_date=$(date -d "+$days days" +%Y-%m-%d)
ensure_firewallfalcon_system_group
if [[ "$adopt_existing" == "true" ]]; then
usermod -s /usr/sbin/nologin "$username" &>/dev/null
else
useradd -m -s /usr/sbin/nologin "$username"
fi
usermod -aG "$FF_USERS_GROUP" "$username" 2>/dev/null
echo "$username:$password" | chpasswd; chage -E "$expire_date" "$username"
echo "$username:$password:$expire_date:$limit:$bandwidth_gb:$daily_bandwidth_gb:trial" >> "$DB_FILE"
local bw_display="Unlimited"
if [[ "$bandwidth_gb" != "0" ]]; then bw_display="${bandwidth_gb} GB"; fi
local daily_bw_display="Unlimited"
if [[ "$daily_bandwidth_gb" != "0" ]]; then daily_bw_display="${daily_bandwidth_gb} GB/day"; fi
clear; show_banner
if [[ "$adopt_existing" == "true" ]]; then
echo -e "${C_GREEN}✅ Existing system user '$username' has been imported into FirewallFalcon!${C_RESET}\n"
else
echo -e "${C_GREEN}✅ User '$username' created successfully!${C_RESET}\n"
fi
echo -e " - 👤 Username: ${C_YELLOW}$username${C_RESET}"
echo -e " - 🔑 Password: ${C_YELLOW}$password${C_RESET}"
echo -e " - 🗓️ Expires on: ${C_YELLOW}$expire_date${C_RESET}"
echo -e " - 📶 Connection Limit: ${C_YELLOW}$limit${C_RESET}"
echo -e " - 📦 Total Bandwidth: ${C_YELLOW}$bw_display${C_RESET}"
echo -e " - 📦 Daily Bandwidth: ${C_YELLOW}$daily_bw_display${C_RESET}"
echo -e " ${C_DIM}(Active monitoring service will enforce these limits)${C_RESET}"
# Auto-ask for config generation
echo
read -p "👉 Do you want to generate a client connection config for this user? (y/n): " gen_conf
if [[ "$gen_conf" == "y" || "$gen_conf" == "Y" ]]; then
generate_client_config "$username" "$password"
fi
invalidate_banner_cache
refresh_dynamic_banner_routing_if_enabled
}
delete_user() {
_select_multi_user_interface "--- 🗑️ Delete FirewallFalcon Users ---" "true"
if [[ ${#SELECTED_USERS[@]} -eq 0 || "${SELECTED_USERS[0]}" == "NO_USERS" ]]; then return; fi
echo -e "\n${C_RED}⚠️ You selected ${#SELECTED_USERS[@]} user(s) to delete: ${C_YELLOW}${SELECTED_USERS[*]}${C_RESET}"
read -p "👉 Are you sure you want to PERMANENTLY delete them? (y/n): " confirm
if [[ "$confirm" != "y" ]]; then echo -e "\n${C_YELLOW}❌ Deletion cancelled.${C_RESET}"; return; fi
echo -e "\n${C_BLUE}🗑️ Deleting selected users...${C_RESET}"
delete_firewallfalcon_user_accounts "${SELECTED_USERS[@]}"
}
edit_user() {
_select_user_interface "--- ✏️ Edit a User ---"
local username=$SELECTED_USER
if [[ "$username" == "NO_USERS" ]] || [[ -z "$username" ]]; then return; fi
while true; do
clear; show_banner; echo -e "${C_BOLD}${C_PURPLE}--- Editing User: ${C_YELLOW}$username${C_PURPLE} ---${C_RESET}"
# Show current user details
local current_line; current_line=$(grep "^$username:" "$DB_FILE")
local cur_pass cur_expiry cur_limit cur_bw cur_daily_bw
IFS=: read -r _ cur_pass cur_expiry cur_limit cur_bw cur_daily_bw _ <<< "$current_line"
[[ -z "$cur_bw" ]] && cur_bw="0"
[[ ! "$cur_daily_bw" =~ ^[0-9]+\.?[0-9]*$ ]] && cur_daily_bw="0"
local cur_bw_display="Unlimited"; [[ "$cur_bw" != "0" ]] && cur_bw_display="${cur_bw} GB"
local cur_daily_bw_display="Unlimited"; [[ "$cur_daily_bw" != "0" ]] && cur_daily_bw_display="${cur_daily_bw} GB/day"
# Show bandwidth usage
local bw_used_display="N/A"
if [[ -f "$BANDWIDTH_DIR/${username}.usage" ]]; then
local used_bytes=0; read -r used_bytes < "$BANDWIDTH_DIR/${username}.usage" 2>/dev/null || used_bytes=0
if [[ -n "$used_bytes" && "$used_bytes" != "0" ]]; then
bw_used_display=$(awk "BEGIN {printf \"%.2f GB\", $used_bytes / 1073741824}")
else
bw_used_display="0.00 GB"
fi
fi
local daily_bw_used_display="N/A"
if [[ -f "$BANDWIDTH_DIR/${username}.daily_usage" ]]; then
local d_used_bytes=0; read -r d_used_bytes < "$BANDWIDTH_DIR/${username}.daily_usage" 2>/dev/null || d_used_bytes=0
if [[ -n "$d_used_bytes" && "$d_used_bytes" != "0" ]]; then
daily_bw_used_display=$(awk "BEGIN {printf \"%.2f GB\", $d_used_bytes / 1073741824}")
else
daily_bw_used_display="0.00 GB"
fi
fi
echo -e "\n ${C_DIM}Current: Pass=${C_YELLOW}$cur_pass${C_RESET}${C_DIM} Exp=${C_YELLOW}$cur_expiry${C_RESET}${C_DIM} Conn=${C_YELLOW}$cur_limit${C_RESET}${C_DIM} BW=${C_YELLOW}$cur_bw_display${C_RESET}${C_DIM} Used=${C_CYAN}$bw_used_display${C_RESET}${C_DIM} Daily BW=${C_YELLOW}$cur_daily_bw_display${C_RESET}${C_DIM} Daily Used=${C_CYAN}$daily_bw_used_display${C_RESET}"
echo -e "\nSelect a detail to edit:\n"
printf " ${C_GREEN}[ 1]${C_RESET} %-35s\n" "🔑 Change Password"
printf " ${C_GREEN}[ 2]${C_RESET} %-35s\n" "🗓️ Change Expiration Date"
printf " ${C_GREEN}[ 3]${C_RESET} %-35s\n" "📶 Change Connection Limit"
printf " ${C_GREEN}[ 4]${C_RESET} %-35s\n" "📦 Change Total Bandwidth Limit"
printf " ${C_GREEN}[ 5]${C_RESET} %-35s\n" "📦 Change Daily Bandwidth Limit"
printf " ${C_GREEN}[ 6]${C_RESET} %-35s\n" "🔄 Reset Bandwidth Counters"
echo -e "\n ${C_RED}[ 0]${C_RESET} ✅ Finish Editing"
echo
if ! read -r -p "👉 Enter your choice: " edit_choice; then
echo
return
fi
case $edit_choice in
1)
local new_pass=""
read -p "Enter new password (or press Enter for auto-generated): " new_pass
if [[ -z "$new_pass" ]]; then
new_pass=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 8)
echo -e "${C_GREEN}🔑 Auto-generated: ${C_YELLOW}$new_pass${C_RESET}"
fi
echo "$username:$new_pass" | chpasswd
sed -i "s/^$username:.*/$username:$new_pass:$cur_expiry:$cur_limit:$cur_bw:$cur_daily_bw/" "$DB_FILE"
echo -e "\n${C_GREEN}✅ Password for '$username' changed to: ${C_YELLOW}$new_pass${C_RESET}"
;;
2) read -p "Enter new duration (in days from today): " days
if [[ "$days" =~ ^[0-9]+$ ]]; then
local new_expire_date; new_expire_date=$(date -d "+$days days" +%Y-%m-%d); chage -E "$new_expire_date" "$username"
sed -i "s/^$username:.*/$username:$cur_pass:$new_expire_date:$cur_limit:$cur_bw:$cur_daily_bw/" "$DB_FILE"
echo -e "\n${C_GREEN}✅ Expiration for '$username' set to ${C_YELLOW}$new_expire_date${C_RESET}."
else echo -e "\n${C_RED}❌ Invalid number of days.${C_RESET}"; fi ;;
3) read -p "Enter new simultaneous connection limit: " new_limit
if [[ "$new_limit" =~ ^[0-9]+$ ]]; then
sed -i "s/^$username:.*/$username:$cur_pass:$cur_expiry:$new_limit:$cur_bw:$cur_daily_bw/" "$DB_FILE"
echo -e "\n${C_GREEN}✅ Connection limit for '$username' set to ${C_YELLOW}$new_limit${C_RESET}."
else echo -e "\n${C_RED}❌ Invalid limit.${C_RESET}"; fi ;;
4) read -p "Enter new TOTAL bandwidth limit in GB (0 = unlimited): " new_bw
if [[ "$new_bw" =~ ^[0-9]+\.?[0-9]*$ ]]; then
sed -i "s/^$username:.*/$username:$cur_pass:$cur_expiry:$cur_limit:$new_bw:$cur_daily_bw/" "$DB_FILE"
local bw_msg="Unlimited"; [[ "$new_bw" != "0" ]] && bw_msg="${new_bw} GB"
echo -e "\n${C_GREEN}✅ Total bandwidth limit for '$username' set to ${C_YELLOW}$bw_msg${C_RESET}."
# Unlock user if they were locked due to bandwidth
if [[ "$new_bw" == "0" ]] || [[ -f "$BANDWIDTH_DIR/${username}.usage" ]]; then
local used_bytes; used_bytes=$(cat "$BANDWIDTH_DIR/${username}.usage" 2>/dev/null || echo 0)
local new_quota_bytes; new_quota_bytes=$(awk "BEGIN {printf \"%.0f\", $new_bw * 1073741824}")
if [[ "$new_bw" == "0" ]] || [[ "$used_bytes" -lt "$new_quota_bytes" ]]; then
usermod -U "$username" &>/dev/null
fi
fi
else echo -e "\n${C_RED}❌ Invalid bandwidth value.${C_RESET}"; fi ;;
5) read -p "Enter new DAILY bandwidth limit in GB (0 = unlimited): " new_daily_bw
if [[ "$new_daily_bw" =~ ^[0-9]+\.?[0-9]*$ ]]; then
sed -i "s/^$username:.*/$username:$cur_pass:$cur_expiry:$cur_limit:$cur_bw:$new_daily_bw/" "$DB_FILE"
local daily_bw_msg="Unlimited"; [[ "$new_daily_bw" != "0" ]] && daily_bw_msg="${new_daily_bw} GB/day"
echo -e "\n${C_GREEN}✅ Daily bandwidth limit for '$username' set to ${C_YELLOW}$daily_bw_msg${C_RESET}."
# Unlock user if they were locked due to daily bandwidth
if [[ "$new_daily_bw" == "0" ]] || [[ -f "$BANDWIDTH_DIR/${username}.daily_usage" ]]; then
local d_used_bytes; d_used_bytes=$(cat "$BANDWIDTH_DIR/${username}.daily_usage" 2>/dev/null || echo 0)
local new_d_quota_bytes; new_d_quota_bytes=$(awk "BEGIN {printf \"%.0f\", $new_daily_bw * 1073741824}")
if [[ "$new_daily_bw" == "0" ]] || [[ "$d_used_bytes" -lt "$new_d_quota_bytes" ]]; then
usermod -U "$username" &>/dev/null
rm -f "$BANDWIDTH_DIR/${username}.daily_locked"
fi
fi
else echo -e "\n${C_RED}❌ Invalid bandwidth value.${C_RESET}"; fi ;;
6)
echo "0" > "$BANDWIDTH_DIR/${username}.usage"
echo "0" > "$BANDWIDTH_DIR/${username}.daily_usage"
rm -f "$BANDWIDTH_DIR/${username}.daily_locked"
# Unlock user if they were locked due to bandwidth
usermod -U "$username" &>/dev/null
echo -e "\n${C_GREEN}✅ All bandwidth counters for '$username' have been reset to 0.${C_RESET}"
;;
0) return ;;
*) echo -e "\n${C_RED}❌ Invalid option.${C_RESET}" ;;
esac
echo -e "\nPress ${C_YELLOW}[Enter]${C_RESET} to continue editing..." && read -r || return
done
}
lock_user() {
_select_multi_user_interface "--- 🔒 Lock Users (from DB) ---"
if [[ ${#SELECTED_USERS[@]} -eq 0 || "${SELECTED_USERS[0]}" == "NO_USERS" ]]; then return; fi
echo -e "\n${C_BLUE}🔒 Locking selected users...${C_RESET}"
for u in "${SELECTED_USERS[@]}"; do
if ! id "$u" &>/dev/null; then
echo -e " ❌ User '${C_YELLOW}$u${C_RESET}' does not exist on this system."
continue
fi
usermod -L "$u"
if [ $? -eq 0 ]; then
killall -u "$u" -9 &>/dev/null
echo -e " ✅ ${C_YELLOW}$u${C_RESET} locked and active sessions killed."
else
echo -e " ❌ Failed to lock ${C_YELLOW}$u${C_RESET}."
fi
done
}
unlock_user() {
_select_multi_user_interface "--- 🔓 Unlock Users (from DB) ---"
if [[ ${#SELECTED_USERS[@]} -eq 0 || "${SELECTED_USERS[0]}" == "NO_USERS" ]]; then return; fi
echo -e "\n${C_BLUE}🔓 Unlocking selected users...${C_RESET}"
for u in "${SELECTED_USERS[@]}"; do
if ! id "$u" &>/dev/null; then
echo -e " ❌ User '${C_YELLOW}$u${C_RESET}' does not exist on this system."
continue
fi
usermod -U "$u"
if [ $? -eq 0 ]; then
echo -e " ✅ ${C_YELLOW}$u${C_RESET} unlocked."
else
echo -e " ❌ Failed to unlock ${C_YELLOW}$u${C_RESET}."
fi
done
}
list_users() {
clear; show_banner
if [[ ! -s "$DB_FILE" ]]; then
echo -e "\n${C_YELLOW}ℹ️ No users are currently being managed.${C_RESET}"
return
fi
echo -e "${C_BOLD}${C_PURPLE}--- 📋 Managed Users ---${C_RESET}"
echo -e "${C_YELLOW}---------------------------------------------------------------------------------------------------${C_RESET}"
printf "${C_BOLD}${C_WHITE}%-18s | %-12s | %-10s | %-25s | %-20s${C_RESET}\n" "USERNAME" "EXPIRATION" "SESSIONS" "BANDWIDTH" "STATUS"
echo -e "${C_YELLOW}---------------------------------------------------------------------------------------------------${C_RESET}"
local current_ts
printf -v current_ts '%(%s)T' -1
local -A system_user_lookup=()
local -A locked_user_lookup=()
while IFS=: read -r system_user _rest; do
[[ -n "$system_user" ]] && system_user_lookup["$system_user"]=1
done < /etc/passwd
if [[ -r /etc/shadow ]]; then
while IFS=: read -r shadow_user shadow_hash _rest; do
[[ -n "$shadow_user" && "${shadow_hash:0:1}" == "!" ]] && locked_user_lookup["$shadow_user"]=1
done < /etc/shadow
else
while read -r passwd_user _ passwd_status _rest; do
[[ -z "$passwd_user" ]] && continue
[[ "$passwd_status" == "L" ]] && locked_user_lookup["$passwd_user"]=1
done < <(passwd -Sa 2>/dev/null)
fi
refresh_ssh_session_cache
while IFS=: read -r user pass expiry limit bandwidth_gb daily_bandwidth_gb _extra; do
local online_count="${SSH_SESSION_COUNTS[$user]:-0}"
local connection_string="$online_count / $limit"
local plain_status="Active"
local status="${C_GREEN}🟢 Active${C_RESET}"
local quota_exceeded=false
[[ -z "$bandwidth_gb" ]] && bandwidth_gb="0"
[[ ! "$daily_bandwidth_gb" =~ ^[0-9]+\.?[0-9]*$ ]] && daily_bandwidth_gb="0"
local bw_string="Unlimited"
local total_str=""
local daily_str=""
if [[ "$bandwidth_gb" != "0" ]]; then
local used_bytes=0
if [[ -f "$BANDWIDTH_DIR/${user}.usage" ]]; then
read -r used_bytes < "$BANDWIDTH_DIR/${user}.usage" 2>/dev/null || used_bytes=0
[[ "$used_bytes" =~ ^[0-9]+$ ]] || used_bytes=0
fi
local used_gb
used_gb=$(awk "BEGIN {printf \"%.1f\", $used_bytes / 1073741824}")
total_str="${used_gb}/${bandwidth_gb}G"
local quota_bytes
quota_bytes=$(awk "BEGIN {printf \"%.0f\", $bandwidth_gb * 1073741824}")
if [[ "$quota_bytes" =~ ^[0-9]+$ ]] && (( used_bytes >= quota_bytes )); then
quota_exceeded=true
fi
fi
if [[ "$daily_bandwidth_gb" != "0" ]]; then
local d_used_bytes=0
if [[ -f "$BANDWIDTH_DIR/${user}.daily_usage" ]]; then
read -r d_used_bytes < "$BANDWIDTH_DIR/${user}.daily_usage" 2>/dev/null || d_used_bytes=0
[[ "$d_used_bytes" =~ ^[0-9]+$ ]] || d_used_bytes=0
fi
local d_used_gb
d_used_gb=$(awk "BEGIN {printf \"%.1f\", $d_used_bytes / 1073741824}")
daily_str="${d_used_gb}/${daily_bandwidth_gb}G/d"
local d_quota_bytes
d_quota_bytes=$(awk "BEGIN {printf \"%.0f\", $daily_bandwidth_gb * 1073741824}")
if [[ "$d_quota_bytes" =~ ^[0-9]+$ ]] && (( d_used_bytes >= d_quota_bytes )); then
quota_exceeded=true
fi
fi
if [[ -n "$total_str" && -n "$daily_str" ]]; then
bw_string="$total_str | $daily_str"
elif [[ -n "$total_str" ]]; then
bw_string="$total_str"
elif [[ -n "$daily_str" ]]; then
bw_string="$daily_str"
fi
if [[ -z "${system_user_lookup[$user]+x}" ]]; then
plain_status="Not Found"
status="${C_RED}Not Found${C_RESET}"
elif [[ -n "$expiry" && "$expiry" != "Never" ]]; then
local expiry_ts
expiry_ts=$(date -d "$expiry" +%s 2>/dev/null || echo 0)
if [[ "$expiry_ts" =~ ^[0-9]+$ ]] && (( expiry_ts > 0 && expiry_ts < current_ts )); then
plain_status="Expired"
status="${C_RED}🗓️ Expired${C_RESET}"
fi
fi
if [[ "$plain_status" == "Active" && "$quota_exceeded" == true ]]; then
if [[ -n "${locked_user_lookup[$user]+x}" ]]; then
plain_status="BW Locked"
status="${C_RED}🔒 BW Locked${C_RESET}"
else
plain_status="Quota Exceeded"
status="${C_RED}📦 Quota Exceeded${C_RESET}"
fi
elif [[ "$plain_status" == "Active" && -n "${locked_user_lookup[$user]+x}" ]]; then
plain_status="Locked"
status="${C_YELLOW}🔒 Locked${C_RESET}"
fi
local line_color="$C_WHITE"
case "$plain_status" in
"Active") line_color="$C_GREEN" ;;
"Locked") line_color="$C_YELLOW" ;;
"Expired") line_color="$C_RED" ;;
"BW Locked") line_color="$C_RED" ;;
"Quota Exceeded") line_color="$C_RED" ;;
"Not Found") line_color="$C_DIM" ;;
esac
printf "${line_color}%-18s ${C_RESET}| ${C_YELLOW}%-12s ${C_RESET}| ${C_CYAN}%-10s ${C_RESET}| ${C_ORANGE}%-25s ${C_RESET}| %-20s\n" "$user" "$expiry" "$connection_string" "$bw_string" "$status"
done < <(sort "$DB_FILE")
echo -e "${C_CYAN}=========================================================================================${C_RESET}\n"
}
renew_user() {
_select_multi_user_interface "--- 🔄 Renew Users ---"
if [[ ${#SELECTED_USERS[@]} -eq 0 || "${SELECTED_USERS[0]}" == "NO_USERS" ]]; then return; fi
read -p "👉 Enter number of days to extend the account(s): " days; if ! [[ "$days" =~ ^[0-9]+$ ]]; then echo -e "\n${C_RED}❌ Invalid number.${C_RESET}"; return; fi
local new_expire_date; new_expire_date=$(date -d "+$days days" +%Y-%m-%d)
echo -e "\n${C_BLUE}🔄 Renewing selected users for $days days...${C_RESET}"
for u in "${SELECTED_USERS[@]}"; do
chage -E "$new_expire_date" "$u"
local line pass _expiry limit bw
line=$(grep "^$u:" "$DB_FILE")
IFS=: read -r _ pass _expiry limit bw _ <<< "$line"
[[ -z "$bw" ]] && bw="0"
sed -i "s/^$u:.*/$u:$pass:$new_expire_date:$limit:$bw/" "$DB_FILE"
echo -e " ✅ ${C_YELLOW}$u${C_RESET} renewed until ${C_GREEN}${new_expire_date}${C_RESET}."
done
}
cleanup_expired() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🧹 Cleanup Expired Users ---${C_RESET}"
local expired_users=()
local current_ts
current_ts=$(date +%s)
if [[ ! -s "$DB_FILE" ]]; then
echo -e "\n${C_GREEN}✅ User database is empty. No expired users found.${C_RESET}"
return
fi
while IFS=: read -r user pass expiry limit bandwidth_gb _extra; do
local expiry_ts
expiry_ts=$(date -d "$expiry" +%s 2>/dev/null || echo 0)
if [[ $expiry_ts -lt $current_ts && $expiry_ts -ne 0 ]]; then
expired_users+=("$user")
fi
done < "$DB_FILE"
if [ ${#expired_users[@]} -eq 0 ]; then
echo -e "\n${C_GREEN}✅ No expired users found.${C_RESET}"
return
fi
echo -e "\nThe following users have expired: ${C_RED}${expired_users[*]}${C_RESET}"
read -p "👉 Do you want to delete all of them? (y/n): " confirm
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
echo -e "\n${C_BLUE}🗑️ Deleting expired users...${C_RESET}"
delete_firewallfalcon_user_accounts "${expired_users[@]}"
echo -e "\n${C_GREEN}✅ Expired users have been cleaned up.${C_RESET}"
else
echo -e "\n${C_YELLOW}❌ Cleanup cancelled.${C_RESET}"
fi
}
backup_user_data() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 💾 Backup User Data ---${C_RESET}"
read -p "👉 Enter path for backup file [/root/firewallfalcon_users.tar.gz]: " backup_path
backup_path=${backup_path:-/root/firewallfalcon_users.tar.gz}
if [ ! -d "$DB_DIR" ] || [ ! -s "$DB_FILE" ]; then
echo -e "\n${C_YELLOW}ℹ️ No user data found to back up.${C_RESET}"
return
fi
echo -e "\n${C_BLUE}⚙️ Backing up user database and settings to ${C_YELLOW}$backup_path${C_RESET}..."
tar -czf "$backup_path" -C "$(dirname "$DB_DIR")" "$(basename "$DB_DIR")"
if [ $? -eq 0 ]; then
echo -e "\n${C_GREEN}✅ SUCCESS: User data backup created at ${C_YELLOW}$backup_path${C_RESET}"
else
echo -e "\n${C_RED}❌ ERROR: Backup failed.${C_RESET}"
fi
}
restore_user_data() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 📥 Restore User Data ---${C_RESET}"
read -p "👉 Enter the full path to the user data backup file [/root/firewallfalcon_users.tar.gz]: " backup_path
backup_path=${backup_path:-/root/firewallfalcon_users.tar.gz}
if [ ! -f "$backup_path" ]; then
echo -e "\n${C_RED}❌ ERROR: Backup file not found at '$backup_path'.${C_RESET}"
return
fi
echo -e "\n${C_RED}${C_BOLD}⚠️ WARNING:${C_RESET} This will overwrite all current users and settings."
echo -e "It will restore user accounts, passwords, limits, and expiration dates from the backup file."
read -p "👉 Are you absolutely sure you want to proceed? (y/n): " confirm
if [[ "$confirm" != "y" ]]; then echo -e "\n${C_YELLOW}❌ Restore cancelled.${C_RESET}"; return; fi
local temp_dir
temp_dir=$(mktemp -d)
echo -e "\n${C_BLUE}⚙️ Extracting backup file to a temporary location...${C_RESET}"
tar -xzf "$backup_path" -C "$temp_dir"
if [ $? -ne 0 ]; then
echo -e "\n${C_RED}❌ ERROR: Failed to extract backup file. Aborting.${C_RESET}"
rm -rf "$temp_dir"
return
fi
local restored_db_file="$temp_dir/firewallfalcon/users.db"
if [ ! -f "$restored_db_file" ]; then
echo -e "\n${C_RED}❌ ERROR: users.db not found in the backup. Cannot restore user accounts.${C_RESET}"
rm -rf "$temp_dir"
return
fi
echo -e "${C_BLUE}⚙️ Overwriting current user database...${C_RESET}"
mkdir -p "$DB_DIR"
cp "$restored_db_file" "$DB_FILE"
if [ -d "$temp_dir/firewallfalcon/ssl" ]; then
cp -r "$temp_dir/firewallfalcon/ssl" "$DB_DIR/"
fi
if [ -d "$temp_dir/firewallfalcon/dnstt" ]; then
cp -r "$temp_dir/firewallfalcon/dnstt" "$DB_DIR/"
fi
if [ -f "$temp_dir/firewallfalcon/dns_info.conf" ]; then
cp "$temp_dir/firewallfalcon/dns_info.conf" "$DB_DIR/"
fi
if [ -f "$temp_dir/firewallfalcon/dnstt_info.conf" ]; then
cp "$temp_dir/firewallfalcon/dnstt_info.conf" "$DB_DIR/"
fi
if [ -f "$temp_dir/firewallfalcon/falconproxy_config.conf" ]; then
cp "$temp_dir/firewallfalcon/falconproxy_config.conf" "$DB_DIR/"
fi
echo -e "${C_BLUE}⚙️ Re-synchronizing system accounts with the restored database...${C_RESET}"
ensure_firewallfalcon_system_group
while IFS=: read -r user pass expiry limit; do
echo "Processing user: ${C_YELLOW}$user${C_RESET}"
if ! id "$user" &>/dev/null; then
echo " - User does not exist in system. Creating..."
useradd -m -s /usr/sbin/nologin "$user"
fi
usermod -aG "$FF_USERS_GROUP" "$user" 2>/dev/null
echo " - Setting password..."
echo "$user:$pass" | chpasswd
echo " - Setting expiration to $expiry..."
chage -E "$expiry" "$user"
echo " - Connection limit is $limit (enforced by PAM)"
done < "$DB_FILE"
rm -rf "$temp_dir"
echo -e "\n${C_GREEN}✅ SUCCESS: User data restore completed.${C_RESET}"
invalidate_banner_cache
refresh_dynamic_banner_routing_if_enabled
}
_enable_banner_in_sshd_config() {
echo -e "\n${C_BLUE}⚙️ Configuring sshd_config...${C_RESET}"
disable_dynamic_ssh_banner_system
sed -i.bak -E 's/^( *Banner *).*/#\1/' /etc/ssh/sshd_config
if ! grep -q -E "^Banner $SSH_BANNER_FILE" /etc/ssh/sshd_config; then
echo -e "\n# FirewallFalcon SSH Banner\nBanner $SSH_BANNER_FILE" >> /etc/ssh/sshd_config
fi
echo -e "${C_GREEN}✅ sshd_config updated.${C_RESET}"
}
_restart_ssh() {
echo -e "\n${C_BLUE}🔄 Restarting SSH service to apply changes...${C_RESET}"
local ssh_service_name=""
if [ -f /lib/systemd/system/sshd.service ]; then
ssh_service_name="sshd.service"
elif [ -f /lib/systemd/system/ssh.service ]; then
ssh_service_name="ssh.service"
else
echo -e "${C_RED}❌ Could not find sshd.service or ssh.service. Cannot restart SSH.${C_RESET}"
return 1
fi
systemctl restart "${ssh_service_name}"
if [ $? -eq 0 ]; then
echo -e "${C_GREEN}✅ SSH service ('${ssh_service_name}') restarted successfully.${C_RESET}"
else
echo -e "${C_RED}❌ Failed to restart SSH service ('${ssh_service_name}'). Please check 'journalctl -u ${ssh_service_name}' for errors.${C_RESET}"
fi
}
set_ssh_banner_paste() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 📋 Paste Static SSH Banner ---${C_RESET}"
echo -e "Paste your custom banner below. Press ${C_YELLOW}[Ctrl+D]${C_RESET} when you are finished."
echo -e "${C_DIM}This will be shown to all SSH users through 'Banner $SSH_BANNER_FILE'.${C_RESET}"
echo -e "${C_DIM}The current banner (if any) will be overwritten.${C_RESET}"
echo -e "--------------------------------------------------"
cat > "$SSH_BANNER_FILE"
chmod 644 "$SSH_BANNER_FILE"
echo -e "\n--------------------------------------------------"
echo -e "\n${C_GREEN}✅ Static banner content saved.${C_RESET}"
_enable_banner_in_sshd_config
_restart_ssh
echo -e "\nPress ${C_YELLOW}[Enter]${C_RESET} to return..." && read -r
}
view_ssh_banner() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 👁️ Current SSH Banner ---${C_RESET}"
if [ -f "$SSH_BANNER_FILE" ]; then
echo -e "\n${C_CYAN}--- BEGIN BANNER ---${C_RESET}"
cat "$SSH_BANNER_FILE"
echo -e "${C_CYAN}---- END BANNER ----${C_RESET}"
else
echo -e "\n${C_YELLOW}ℹ️ No banner file found at $SSH_BANNER_FILE.${C_RESET}"
fi
echo -e "\nPress ${C_YELLOW}[Enter]${C_RESET} to return..." && read -r
}
remove_ssh_banner() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🗑️ Disable SSH Banners ---${C_RESET}"
read -p "👉 Are you sure you want to disable all SSH banners? (y/n): " confirm
if [[ "$confirm" != "y" ]]; then
echo -e "\n${C_YELLOW}❌ Action cancelled.${C_RESET}"
echo -e "\nPress ${C_YELLOW}[Enter]${C_RESET} to return..." && read -r
return
fi
if [ -f "$SSH_BANNER_FILE" ]; then
rm -f "$SSH_BANNER_FILE"
echo -e "\n${C_GREEN}✅ Removed banner file: $SSH_BANNER_FILE${C_RESET}"
else
echo -e "\n${C_YELLOW}ℹ️ No banner file to remove.${C_RESET}"
fi
disable_dynamic_ssh_banner_system
echo -e "\n${C_BLUE}⚙️ Disabling banner in sshd_config...${C_RESET}"
disable_static_ssh_banner_in_sshd_config
echo -e "${C_GREEN}✅ Banner disabled in configuration.${C_RESET}"
_restart_ssh
echo -e "\nPress ${C_YELLOW}[Enter]${C_RESET} to return..." && read -r
}
preview_dynamic_ssh_banner() {
if ! is_dynamic_ssh_banner_enabled; then
echo -e "\n${C_RED}❌ Dynamic banners are not enabled right now.${C_RESET}"
press_enter
return
fi
echo -e "${C_DIM}Refreshing dynamic banner worker...${C_RESET}"
setup_limiter_service >/dev/null 2>&1
_select_user_interface "--- 📝 Preview Dynamic Banner ---"
local u=$SELECTED_USER
if [[ -z "$u" || "$u" == "NO_USERS" ]]; then
return
fi
echo -e "\n${C_CYAN}--- Dynamic Banner Preview for user '$u' ---${C_RESET}\n"
if [[ -f "/etc/firewallfalcon/banners/${u}.txt" ]]; then
cat "/etc/firewallfalcon/banners/${u}.txt"
else
echo -e "${C_RED}Banner file not generated yet. Waiting up to 10s for the worker...${C_RESET}"
sleep 5
if ! cat "/etc/firewallfalcon/banners/${u}.txt" 2>/dev/null; then
echo -e "\n${C_RED}Still not generated. Here are the last limiter logs:${C_RESET}"
echo -e "----------------------------------------------------------------------"
journalctl -u firewallfalcon-limiter -n 15 --no-pager
echo -e "----------------------------------------------------------------------"
fi
fi
press_enter
}
# NOTE: The full ssh_banner_menu() with dynamic/static support is defined later in the file.
install_udp_custom() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🚀 Installing udp-custom ---${C_RESET}"
if [ -f "$UDP_CUSTOM_SERVICE_FILE" ] || [ -f "$UDPGW_SERVICE_FILE" ]; then
echo -e "\n${C_YELLOW}ℹ️ udp-custom is already installed.${C_RESET}"
return
fi
check_and_free_ports 36712 7800 || return
check_and_open_firewall_port 36712 udp || return
echo -e "\n${C_GREEN}⚙️ Creating directory for udp-custom...${C_RESET}"
rm -rf "$UDP_CUSTOM_DIR"
mkdir -p "$UDP_CUSTOM_DIR"
echo -e "\n${C_GREEN}⚙️ Detecting system architecture...${C_RESET}"
local arch
arch=$(uname -m)
local binary_source=""
if [[ "$arch" == "x86_64" ]]; then
binary_source="udp-custom-linux-amd64"
echo -e "${C_BLUE}ℹ️ Detected x86_64 (amd64) architecture.${C_RESET}"
elif [[ "$arch" == "aarch64" || "$arch" == "arm64" ]]; then
binary_source="udp-custom-linux-arm"
echo -e "${C_BLUE}ℹ️ Detected ARM64 architecture.${C_RESET}"
else
echo -e "\n${C_RED}❌ Unsupported architecture: $arch. Cannot install udp-custom.${C_RESET}"
rm -rf "$UDP_CUSTOM_DIR"
return
fi
echo -e "\n${C_GREEN}📥 Installing udp-custom binary from local bundle...${C_RESET}"
if ! ff_require_bundle_file "$FF_BUNDLE_DIR/udp/$binary_source"; then
rm -rf "$UDP_CUSTOM_DIR"
return
fi
echo -e "${C_BLUE}ℹ️ Using local bundle copy: $binary_source${C_RESET}"
cp "$FF_BUNDLE_DIR/udp/$binary_source" "$UDP_CUSTOM_DIR/udp-custom"
chmod +x "$UDP_CUSTOM_DIR/udp-custom"
echo -e "\n${C_GREEN}📦 Setting up udpgw helper...${C_RESET}"
# The bundled udpgw is an x86-64 build; ARM uses an optional arm64 bundle copy,
# otherwise it is compiled from source.
local udpgw_source=""
if [[ "$arch" == "x86_64" ]]; then
udpgw_source="$FF_BUNDLE_DIR/udp/udpgw"
if ! ff_require_bundle_file "$udpgw_source"; then
rm -rf "$UDP_CUSTOM_DIR"
return
fi
elif [[ -f "$FF_BUNDLE_DIR/udp/udpgw-linux-arm64" ]]; then
udpgw_source="$FF_BUNDLE_DIR/udp/udpgw-linux-arm64"
fi
if [[ -n "$udpgw_source" ]]; then
echo -e "${C_BLUE}ℹ️ Using local bundle copy: $(basename "$udpgw_source")${C_RESET}"
cp "$udpgw_source" "$UDPGW_BINARY"
if [ ! -s "$UDPGW_BINARY" ]; then
echo -e "\n${C_RED}❌ Failed to obtain the udpgw helper binary.${C_RESET}"
rm -rf "$UDP_CUSTOM_DIR"
return
fi
chmod +x "$UDPGW_BINARY"
else
echo -e "${C_YELLOW}ℹ️ Architecture is $arch and no bundled arm64 udpgw was found. Compiling udpgw from source (needs internet, this may take a minute)...${C_RESET}"
ff_pkg_install cmake g++ make git >/dev/null 2>&1
local temp_build="/tmp/badvpn_build"
rm -rf "$temp_build"
git clone -q https://github.com/ambrop72/badvpn.git "$temp_build"
(cd "$temp_build" && cmake . >/dev/null 2>&1 && make >/dev/null 2>&1)
local compiled_bin=$(find "$temp_build" -name "badvpn-udpgw" -type f | head -n 1)
if [[ -n "$compiled_bin" && -f "$compiled_bin" ]]; then
cp "$compiled_bin" "$UDPGW_BINARY"
chmod +x "$UDPGW_BINARY"
else
echo -e "\n${C_RED}❌ Failed to compile udpgw helper for $arch.${C_RESET}"
rm -rf "$UDP_CUSTOM_DIR" "$temp_build"
return
fi
rm -rf "$temp_build"
fi
echo -e "\n${C_GREEN}📝 Creating default config.json...${C_RESET}"
cat > "$UDP_CUSTOM_DIR/config.json" < "$UDPGW_SERVICE_FILE" < "$UDP_CUSTOM_SERVICE_FILE" </dev/null 2>&1
systemctl disable udpgw.service >/dev/null 2>&1
echo -e "${C_GREEN}🛑 Stopping and disabling udp-custom service...${C_RESET}"
systemctl stop udp-custom.service >/dev/null 2>&1
systemctl disable udp-custom.service >/dev/null 2>&1
echo -e "${C_GREEN}🗑️ Removing systemd service file...${C_RESET}"
rm -f "$UDP_CUSTOM_SERVICE_FILE"
rm -f "$UDPGW_SERVICE_FILE"
systemctl daemon-reload
echo -e "${C_GREEN}🗑️ Removing udp-custom directory and files...${C_RESET}"
rm -rf "$UDP_CUSTOM_DIR"
rm -f "$UDPGW_BINARY"
echo -e "${C_GREEN}✅ udp-custom has been uninstalled successfully.${C_RESET}"
}
ensure_badvpn_service_is_quiet() {
if [[ ! -f "$BADVPN_SERVICE_FILE" ]] || grep -q "^StandardOutput=null$" "$BADVPN_SERVICE_FILE" 2>/dev/null; then
return
fi
local tmp_service
tmp_service=$(mktemp)
awk '
/^\[Service\]$/ {
print
print "StandardOutput=null"
print "StandardError=null"
next
}
{ print }
' "$BADVPN_SERVICE_FILE" > "$tmp_service" && mv "$tmp_service" "$BADVPN_SERVICE_FILE"
rm -f "$tmp_service" 2>/dev/null
systemctl daemon-reload
systemctl restart badvpn.service >/dev/null 2>&1 || true
}
install_badvpn() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🚀 Installing badvpn (udpgw) ---${C_RESET}"
if [ -f "$BADVPN_SERVICE_FILE" ]; then
echo -e "\n${C_YELLOW}ℹ️ badvpn is already installed.${C_RESET}"
return
fi
check_and_open_firewall_port 7300 udp || return
echo -e "\n${C_GREEN}🔄 Updating package lists...${C_RESET}"
ff_apt_update || return
echo -e "\n${C_GREEN}📦 Installing all required packages...${C_RESET}"
ff_pkg_install cmake g++ make screen git build-essential libssl-dev libnspr4-dev libnss3-dev pkg-config || {
echo -e "${C_RED}❌ Failed to install badvpn build dependencies.${C_RESET}"
return
}
echo -e "\n${C_GREEN}📥 Cloning badvpn from github...${C_RESET}"
git clone https://github.com/ambrop72/badvpn.git "$BADVPN_BUILD_DIR"
cd "$BADVPN_BUILD_DIR" || { echo -e "${C_RED}❌ Failed to change directory to build folder.${C_RESET}"; return; }
echo -e "\n${C_GREEN}⚙️ Running CMake...${C_RESET}"
cmake . || { echo -e "${C_RED}❌ CMake configuration failed.${C_RESET}"; rm -rf "$BADVPN_BUILD_DIR"; return; }
echo -e "\n${C_GREEN}🛠️ Compiling source...${C_RESET}"
make || { echo -e "${C_RED}❌ Compilation (make) failed.${C_RESET}"; rm -rf "$BADVPN_BUILD_DIR"; return; }
local badvpn_binary
badvpn_binary=$(find "$BADVPN_BUILD_DIR" -name "badvpn-udpgw" -type f | head -n 1)
if [[ -z "$badvpn_binary" || ! -f "$badvpn_binary" ]]; then
echo -e "${C_RED}❌ ERROR: Could not find the compiled 'badvpn-udpgw' binary after compilation.${C_RESET}"
rm -rf "$BADVPN_BUILD_DIR"
return
fi
echo -e "${C_GREEN}ℹ️ Found binary at: $badvpn_binary${C_RESET}"
chmod +x "$badvpn_binary"
echo -e "\n${C_GREEN}📝 Creating systemd service file...${C_RESET}"
cat > "$BADVPN_SERVICE_FILE" <<-EOF
[Unit]
Description=BadVPN UDP Gateway
After=network.target
[Service]
ExecStart=$badvpn_binary --listen-addr 0.0.0.0:7300 --max-clients 1000 --max-connections-for-client 8
User=root
Restart=always
RestartSec=3
StandardOutput=null
StandardError=null
[Install]
WantedBy=multi-user.target
EOF
echo -e "\n${C_GREEN}▶️ Enabling and starting badvpn service...${C_RESET}"
systemctl daemon-reload
systemctl enable badvpn.service
systemctl start badvpn.service
sleep 2
if systemctl is-active --quiet badvpn; then
echo -e "\n${C_GREEN}✅ SUCCESS: badvpn (udpgw) is installed and active on port 7300.${C_RESET}"
else
echo -e "\n${C_RED}❌ ERROR: badvpn service failed to start.${C_RESET}"
echo -e "${C_YELLOW}ℹ️ Displaying last 15 lines of the service log for diagnostics:${C_RESET}"
journalctl -u badvpn.service -n 15 --no-pager
fi
}
uninstall_badvpn() {
echo -e "\n${C_BOLD}${C_PURPLE}--- 🗑️ Uninstalling badvpn (udpgw) ---${C_RESET}"
if [ ! -f "$BADVPN_SERVICE_FILE" ]; then
echo -e "${C_YELLOW}ℹ️ badvpn is not installed, skipping.${C_RESET}"
return
fi
echo -e "${C_GREEN}🛑 Stopping and disabling badvpn service...${C_RESET}"
systemctl stop badvpn.service >/dev/null 2>&1
systemctl disable badvpn.service >/dev/null 2>&1
echo -e "${C_GREEN}🗑️ Removing systemd service file...${C_RESET}"
rm -f "$BADVPN_SERVICE_FILE"
systemctl daemon-reload
echo -e "${C_GREEN}🗑️ Removing badvpn build directory...${C_RESET}"
rm -rf "$BADVPN_BUILD_DIR"
echo -e "${C_GREEN}✅ badvpn has been uninstalled successfully.${C_RESET}"
}
load_edge_cert_info() {
EDGE_CERT_MODE=""
EDGE_DOMAIN=""
EDGE_EMAIL=""
if [ -f "$EDGE_CERT_INFO_FILE" ]; then
source "$EDGE_CERT_INFO_FILE"
fi
}
save_edge_cert_info() {
local cert_mode="$1"
local cert_domain="$2"
local cert_email="$3"
mkdir -p "$DB_DIR"
cat > "$EDGE_CERT_INFO_FILE" </dev/null | head -n 1)
if [[ "$nginx_domain" != "_" && -n "$nginx_domain" ]]; then
host_domain="$nginx_domain"
fi
fi
if [[ -z "$host_domain" ]]; then
host_domain=$(curl -s -4 icanhazip.com)
fi
echo "$host_domain"
}
backup_edge_configs() {
if [ -f "$NGINX_CONFIG_FILE" ] && [ ! -f "${NGINX_CONFIG_FILE}.bak.firewallfalcon" ]; then
cp "$NGINX_CONFIG_FILE" "${NGINX_CONFIG_FILE}.bak.firewallfalcon" 2>/dev/null
fi
if [ -f "$HAPROXY_CONFIG" ] && [ ! -f "${HAPROXY_CONFIG}.bak.firewallfalcon" ]; then
cp "$HAPROXY_CONFIG" "${HAPROXY_CONFIG}.bak.firewallfalcon" 2>/dev/null
fi
}
ensure_edge_stack_packages() {
local missing_packages=()
if ! command -v haproxy &> /dev/null || [ ! -f "/etc/haproxy/haproxy.cfg" ]; then
missing_packages+=("haproxy")
fi
if ! command -v nginx &> /dev/null || [ ! -f "/etc/nginx/nginx.conf" ]; then
missing_packages+=("nginx")
fi
command -v openssl &> /dev/null || missing_packages+=("openssl")
if (( ${#missing_packages[@]} > 0 )); then
echo -e "\n${C_BLUE}📦 Installing required packages: ${missing_packages[*]}${C_RESET}"
if ! ff_pkg_install "${missing_packages[@]}"; then
echo -e "${C_YELLOW}⚠️ Package installation failed. Attempting to fix broken configurations...${C_RESET}"
if command -v apt-get &>/dev/null; then
apt-get purge -y nginx nginx-common haproxy >/dev/null 2>&1
fi
if ! ff_pkg_install "${missing_packages[@]}"; then
echo -e "${C_RED}❌ Failed to install the required packages.${C_RESET}"
return 1
fi
fi
fi
return 0
}
build_shared_tls_bundle() {
if [ ! -s "$SSL_CERT_CHAIN_FILE" ] || [ ! -s "$SSL_CERT_KEY_FILE" ]; then
echo -e "${C_RED}❌ Certificate chain or key is missing.${C_RESET}"
return 1
fi
cat "$SSL_CERT_CHAIN_FILE" "$SSL_CERT_KEY_FILE" > "$SSL_CERT_FILE" || return 1
chmod 644 "$SSL_CERT_CHAIN_FILE"
chmod 600 "$SSL_CERT_KEY_FILE" "$SSL_CERT_FILE"
return 0
}
generate_self_signed_edge_cert() {
local common_name="$1"
mkdir -p "$SSL_CERT_DIR"
echo -e "\n${C_GREEN}🔐 Generating a shared self-signed certificate...${C_RESET}"
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
-keyout "$SSL_CERT_KEY_FILE" \
-out "$SSL_CERT_CHAIN_FILE" \
-subj "/CN=$common_name" \
>/dev/null 2>&1 || {
echo -e "${C_RED}❌ Failed to generate the self-signed certificate.${C_RESET}"
return 1
}
build_shared_tls_bundle || return 1
save_edge_cert_info "self-signed" "$common_name" ""
echo -e "${C_GREEN}✅ Shared certificate created for ${C_YELLOW}$common_name${C_RESET}"
return 0
}
_install_certbot() {
if command -v certbot &> /dev/null; then
echo -e "${C_GREEN}✅ Certbot is already installed.${C_RESET}"
return 0
fi
echo -e "${C_BLUE}📦 Installing Certbot...${C_RESET}"
ff_pkg_install certbot || {
echo -e "${C_RED}❌ Failed to install Certbot.${C_RESET}"
return 1
}
echo -e "${C_GREEN}✅ Certbot installed successfully.${C_RESET}"
return 0
}
obtain_certbot_edge_cert() {
local domain_name="$1"
local email="$2"
local restart_haproxy=0
local restart_nginx=0
mkdir -p "$SSL_CERT_DIR"
_install_certbot || return 1
if systemctl is-active --quiet haproxy; then restart_haproxy=1; fi
if systemctl is-active --quiet nginx; then restart_nginx=1; fi
echo -e "\n${C_BLUE}🛑 Stopping HAProxy and Nginx for Certbot validation...${C_RESET}"
systemctl stop haproxy >/dev/null 2>&1
systemctl stop nginx >/dev/null 2>&1
sleep 2
check_and_free_ports "$EDGE_PUBLIC_HTTP_PORT" "$EDGE_PUBLIC_TLS_PORT" || {
[[ "$restart_nginx" -eq 1 ]] && systemctl start nginx >/dev/null 2>&1
[[ "$restart_haproxy" -eq 1 ]] && systemctl start haproxy >/dev/null 2>&1
return 1
}
echo -e "\n${C_BLUE}🚀 Requesting a Certbot certificate for ${C_YELLOW}$domain_name${C_RESET}"
certbot certonly --standalone -d "$domain_name" --non-interactive --agree-tos -m "$email"
if [ $? -ne 0 ]; then
echo -e "\n${C_RED}❌ Certbot failed to obtain a certificate.${C_RESET}"
echo -e "${C_YELLOW}ℹ️ Make sure the domain points to this server and port 80 is reachable.${C_RESET}"
[[ "$restart_nginx" -eq 1 ]] && systemctl start nginx >/dev/null 2>&1
[[ "$restart_haproxy" -eq 1 ]] && systemctl start haproxy >/dev/null 2>&1
return 1
fi
local certbot_chain="/etc/letsencrypt/live/$domain_name/fullchain.pem"
local certbot_key="/etc/letsencrypt/live/$domain_name/privkey.pem"
if [ ! -f "$certbot_chain" ] || [ ! -f "$certbot_key" ]; then
echo -e "\n${C_RED}❌ Certbot completed, but the certificate files were not found.${C_RESET}"
[[ "$restart_nginx" -eq 1 ]] && systemctl start nginx >/dev/null 2>&1
[[ "$restart_haproxy" -eq 1 ]] && systemctl start haproxy >/dev/null 2>&1
return 1
fi
cp "$certbot_chain" "$SSL_CERT_CHAIN_FILE"
cp "$certbot_key" "$SSL_CERT_KEY_FILE"
build_shared_tls_bundle || {
[[ "$restart_nginx" -eq 1 ]] && systemctl start nginx >/dev/null 2>&1
[[ "$restart_haproxy" -eq 1 ]] && systemctl start haproxy >/dev/null 2>&1
return 1
}
save_edge_cert_info "certbot" "$domain_name" "$email"
echo -e "${C_GREEN}✅ Certbot certificate copied into ${C_YELLOW}$SSL_CERT_DIR${C_RESET}"
return 0
}
select_edge_certificate() {
local preferred_host
local cert_choice
local has_existing_cert=false
preferred_host=$(detect_preferred_host)
if [[ -z "$preferred_host" ]]; then
preferred_host="firewallfalcon.local"
fi
if [ -s "$SSL_CERT_FILE" ] && [ -s "$SSL_CERT_CHAIN_FILE" ] && [ -s "$SSL_CERT_KEY_FILE" ]; then
has_existing_cert=true
fi
load_edge_cert_info
echo -e "\n${C_BOLD}${C_PURPLE}--- 🔐 Shared TLS Certificate ---${C_RESET}"
echo -e "${C_DIM}The same certificate will be used by HAProxy and the internal Nginx proxy.${C_RESET}"
if $has_existing_cert; then
local existing_label="${EDGE_CERT_MODE:-existing}"
if [[ -n "$EDGE_DOMAIN" ]]; then
existing_label="$existing_label - $EDGE_DOMAIN"
fi
printf " ${C_CHOICE}[ 1]${C_RESET} %-52s\n" "Reuse existing certificate (${existing_label})"
printf " ${C_CHOICE}[ 2]${C_RESET} %-52s\n" "Replace with a new self-signed certificate"
printf " ${C_CHOICE}[ 3]${C_RESET} %-52s\n" "Replace with a Certbot certificate"
echo
read -p "👉 Enter choice [1]: " cert_choice
cert_choice=${cert_choice:-1}
else
printf " ${C_CHOICE}[ 1]${C_RESET} %-52s\n" "Generate a self-signed certificate"
printf " ${C_CHOICE}[ 2]${C_RESET} %-52s\n" "Use a Certbot certificate"
echo
read -p "👉 Enter choice [1]: " cert_choice
cert_choice=${cert_choice:-1}
fi
case "$cert_choice" in
1)
if $has_existing_cert; then
echo -e "${C_GREEN}✅ Reusing the existing shared certificate.${C_RESET}"
return 0
fi
local common_name
read -p "👉 Enter the certificate Common Name / SNI label [$preferred_host]: " common_name
common_name=${common_name:-$preferred_host}
generate_self_signed_edge_cert "$common_name"
;;
2)
if $has_existing_cert; then
local common_name
read -p "👉 Enter the certificate Common Name / SNI label [$preferred_host]: " common_name
common_name=${common_name:-$preferred_host}
generate_self_signed_edge_cert "$common_name"
else
local default_domain=""
local domain_name
local email
if ! _is_valid_ipv4 "$preferred_host"; then
default_domain="$preferred_host"
fi
if [[ -n "$default_domain" ]]; then
read -p "👉 Enter your domain name [$default_domain]: " domain_name
domain_name=${domain_name:-$default_domain}
else
read -p "👉 Enter your domain name (e.g. vpn.example.com): " domain_name
fi
if [[ -z "$domain_name" ]]; then
echo -e "${C_RED}❌ Domain name cannot be empty.${C_RESET}"
return 1
fi
if _is_valid_ipv4 "$domain_name"; then
echo -e "${C_RED}❌ Certbot requires a real domain name, not a raw IP address.${C_RESET}"
return 1
fi
read -p "👉 Enter your email for Let's Encrypt: " email
if [[ -z "$email" ]]; then
echo -e "${C_RED}❌ Email cannot be empty.${C_RESET}"
return 1
fi
obtain_certbot_edge_cert "$domain_name" "$email"
fi
;;
3)
if ! $has_existing_cert; then
echo -e "${C_RED}❌ Invalid option.${C_RESET}"
return 1
fi
local default_domain=""
local domain_name
local email
if [[ -n "$EDGE_DOMAIN" ]] && ! _is_valid_ipv4 "$EDGE_DOMAIN"; then
default_domain="$EDGE_DOMAIN"
fi
if [[ -z "$default_domain" ]] && ! _is_valid_ipv4 "$preferred_host"; then
default_domain="$preferred_host"
fi
if [[ -n "$default_domain" ]]; then
read -p "👉 Enter your domain name [$default_domain]: " domain_name
domain_name=${domain_name:-$default_domain}
else
read -p "👉 Enter your domain name (e.g. vpn.example.com): " domain_name
fi
if [[ -z "$domain_name" ]]; then
echo -e "${C_RED}❌ Domain name cannot be empty.${C_RESET}"
return 1
fi
if _is_valid_ipv4 "$domain_name"; then
echo -e "${C_RED}❌ Certbot requires a real domain name, not a raw IP address.${C_RESET}"
return 1
fi
read -p "👉 Enter your email for Let's Encrypt [${EDGE_EMAIL}]: " email
email=${email:-$EDGE_EMAIL}
if [[ -z "$email" ]]; then
echo -e "${C_RED}❌ Email cannot be empty.${C_RESET}"
return 1
fi
obtain_certbot_edge_cert "$domain_name" "$email"
;;
*)
echo -e "${C_RED}❌ Invalid option.${C_RESET}"
return 1
;;
esac
}
write_internal_nginx_config() {
local server_name="$1"
[[ -z "$server_name" ]] && server_name="_"
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
cat > "$NGINX_CONFIG_FILE" <\d+)/(?.*)$ {
client_max_body_size 0;
client_body_timeout 1d;
grpc_read_timeout 1d;
grpc_socket_keepalive on;
proxy_read_timeout 1d;
proxy_http_version 1.1;
proxy_buffering off;
proxy_request_buffering off;
proxy_socket_keepalive on;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
if (\$content_type ~* "GRPC") { grpc_pass grpc://127.0.0.1:\$fwdport\$is_args\$args; break; }
proxy_pass http://127.0.0.1:\$fwdport\$is_args\$args;
break;
}
location / {
proxy_read_timeout 3600s;
proxy_buffering off;
proxy_request_buffering off;
proxy_http_version 1.1;
proxy_socket_keepalive on;
tcp_nodelay on;
tcp_nopush off;
proxy_pass http://127.0.0.1:8080;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
}
}
EOF
ln -sf "$NGINX_CONFIG_FILE" /etc/nginx/sites-enabled/default
}
write_haproxy_edge_config() {
mkdir -p /etc/haproxy
cat > "$HAPROXY_CONFIG" < "$NGINX_PORTS_FILE" </dev/null 2>&1; then
echo -e "${C_RED}❌ Nginx configuration validation failed.${C_RESET}"
nginx -t
return 1
fi
echo -e "${C_BLUE}🧪 Validating HAProxy configuration...${C_RESET}"
if ! haproxy -c -f "$HAPROXY_CONFIG" >/dev/null 2>&1; then
echo -e "${C_RED}❌ HAProxy configuration validation failed.${C_RESET}"
haproxy -c -f "$HAPROXY_CONFIG"
return 1
fi
systemctl daemon-reload
systemctl enable nginx >/dev/null 2>&1
systemctl enable haproxy >/dev/null 2>&1
echo -e "\n${C_BLUE}▶️ Restarting internal Nginx...${C_RESET}"
systemctl restart nginx || {
echo -e "${C_RED}❌ Nginx failed to restart.${C_RESET}"
systemctl status nginx --no-pager
return 1
}
echo -e "${C_BLUE}▶️ Restarting HAProxy edge...${C_RESET}"
systemctl restart haproxy || {
echo -e "${C_RED}❌ HAProxy failed to restart.${C_RESET}"
systemctl status haproxy --no-pager
return 1
}
sleep 2
if ! systemctl is-active --quiet nginx; then
echo -e "${C_RED}❌ Nginx is not active after restart.${C_RESET}"
systemctl status nginx --no-pager
return 1
fi
if ! systemctl is-active --quiet haproxy; then
echo -e "${C_RED}❌ HAProxy is not active after restart.${C_RESET}"
systemctl status haproxy --no-pager
return 1
fi
save_edge_ports_info
return 0
}
install_ssl_tunnel() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🚀 Installing HAProxy Edge Stack (80/443 -> 8880/8443) ---${C_RESET}"
echo -e "\n${C_CYAN}This installer will configure:${C_RESET}"
echo -e " • HAProxy on ${C_WHITE}${EDGE_PUBLIC_HTTP_PORT}/${EDGE_PUBLIC_TLS_PORT}${C_RESET}"
echo -e " • Internal Nginx on ${C_WHITE}${NGINX_INTERNAL_HTTP_PORT}/${NGINX_INTERNAL_TLS_PORT}${C_RESET}"
echo -e " • Loopback SSL decryptor on ${C_WHITE}${HAPROXY_INTERNAL_DECRYPT_PORT}${C_RESET}"
if [ -f "$HAPROXY_CONFIG" ] || [ -f "$NGINX_CONFIG_FILE" ]; then
echo -e "\n${C_YELLOW}⚠️ Existing HAProxy/Nginx configs will be replaced with the FirewallFalcon edge layout.${C_RESET}"
read -p "👉 Continue with the replacement? (y/n): " confirm_replace
if [[ "$confirm_replace" != "y" && "$confirm_replace" != "Y" ]]; then
echo -e "${C_RED}❌ Installation cancelled.${C_RESET}"
return
fi
fi
mkdir -p "$DB_DIR" "$SSL_CERT_DIR"
ensure_edge_stack_packages || return
systemctl stop haproxy >/dev/null 2>&1
systemctl stop nginx >/dev/null 2>&1
sleep 1
check_and_free_ports \
"$EDGE_PUBLIC_HTTP_PORT" \
"$EDGE_PUBLIC_TLS_PORT" \
"$NGINX_INTERNAL_HTTP_PORT" \
"$NGINX_INTERNAL_TLS_PORT" \
"$HAPROXY_INTERNAL_DECRYPT_PORT" || return
check_and_open_firewall_port "$EDGE_PUBLIC_HTTP_PORT" tcp || return
check_and_open_firewall_port "$EDGE_PUBLIC_TLS_PORT" tcp || return
select_edge_certificate || return
load_edge_cert_info
local server_name="${EDGE_DOMAIN:-$(detect_preferred_host)}"
[[ -z "$server_name" ]] && server_name="_"
configure_edge_stack "$server_name" || return
echo -e "\n${C_GREEN}✅ SUCCESS: HAProxy edge stack is active.${C_RESET}"
echo -e " • Public edge ports: ${C_YELLOW}${EDGE_PUBLIC_HTTP_PORT}/${EDGE_PUBLIC_TLS_PORT}${C_RESET}"
echo -e " • Internal Nginx ports: ${C_YELLOW}${NGINX_INTERNAL_HTTP_PORT}/${NGINX_INTERNAL_TLS_PORT}${C_RESET}"
echo -e " • Shared certificate: ${C_YELLOW}${EDGE_CERT_MODE:-unknown}${C_RESET}"
}
uninstall_ssl_tunnel() {
echo -e "\n${C_BOLD}${C_PURPLE}--- 🗑️ Uninstalling HAProxy Edge Stack ---${C_RESET}"
if ! command -v haproxy &> /dev/null; then
echo -e "${C_YELLOW}ℹ️ HAProxy is not installed, skipping service removal.${C_RESET}"
else
echo -e "${C_GREEN}🛑 Stopping and disabling HAProxy...${C_RESET}"
systemctl stop haproxy >/dev/null 2>&1
systemctl disable haproxy >/dev/null 2>&1
fi
if [ -f "$HAPROXY_CONFIG" ]; then
cat > "$HAPROXY_CONFIG" </dev/null 2>&1
fi
rm -f "$SSL_CERT_FILE" "$SSL_CERT_CHAIN_FILE" "$SSL_CERT_KEY_FILE" "$EDGE_CERT_INFO_FILE"
rm -f "$NGINX_PORTS_FILE"
echo -e "${C_GREEN}🗑️ Shared certificate files removed.${C_RESET}"
fi
echo -e "${C_GREEN}✅ HAProxy edge stack has been removed.${C_RESET}"
if systemctl is-active --quiet nginx; then
echo -e "${C_DIM}The internal Nginx proxy is still installed on ${NGINX_INTERNAL_HTTP_PORT}/${NGINX_INTERNAL_TLS_PORT}.${C_RESET}"
fi
}
show_dnstt_details() {
if [ -f "$DNSTT_CONFIG_FILE" ]; then
source "$DNSTT_CONFIG_FILE"
echo -e "\n${C_GREEN}=====================================================${C_RESET}"
echo -e "${C_GREEN} 📡 DNSTT Connection Details ${C_RESET}"
echo -e "${C_GREEN}=====================================================${C_RESET}"
echo -e "\n${C_WHITE}Your connection details:${C_RESET}"
echo -e " - ${C_CYAN}Tunnel Domain:${C_RESET} ${C_YELLOW}$TUNNEL_DOMAIN${C_RESET}"
echo -e " - ${C_CYAN}Public Key:${C_RESET} ${C_YELLOW}$PUBLIC_KEY${C_RESET}"
if [[ -n "$FORWARD_DESC" ]]; then
echo -e " - ${C_CYAN}Forwarding To:${C_RESET} ${C_YELLOW}$FORWARD_DESC${C_RESET}"
else
echo -e " - ${C_CYAN}Forwarding To:${C_RESET} ${C_YELLOW}Unknown (config_missing)${C_RESET}"
fi
if [[ -n "$MTU_VALUE" ]]; then
echo -e " - ${C_CYAN}MTU Value:${C_RESET} ${C_YELLOW}$MTU_VALUE${C_RESET}"
fi
if [[ "$DNSTT_RECORDS_MANAGED" == "false" && -n "$NS_DOMAIN" ]]; then
echo -e " - ${C_CYAN}NS Record:${C_RESET} ${C_YELLOW}$NS_DOMAIN${C_RESET}"
fi
if [[ "$FORWARD_DESC" == *"V2Ray"* ]]; then
echo -e " - ${C_CYAN}Action Required:${C_RESET} ${C_YELLOW}Ensure a V2Ray service (vless/vmess/trojan) listens on port 8787 (no TLS)${C_RESET}"
elif [[ "$FORWARD_DESC" == *"SSH"* ]]; then
echo -e " - ${C_CYAN}Action Required:${C_RESET} ${C_YELLOW}Ensure your SSH client is configured to use the DNS tunnel.${C_RESET}"
fi
echo -e "\n${C_DIM}Use these details in your client configuration.${C_RESET}"
else
echo -e "\n${C_YELLOW}ℹ️ DNSTT configuration file not found. Details are unavailable.${C_RESET}"
fi
}
install_dnstt() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 📡 DNSTT (DNS Tunnel) Management ---${C_RESET}"
if [ -f "$DNSTT_SERVICE_FILE" ]; then
echo -e "\n${C_YELLOW}ℹ️ DNSTT is already installed.${C_RESET}"
show_dnstt_details
return
fi
# --- FIX: Force release of Port 53 / Disable systemd-resolved ---
echo -e "${C_GREEN}⚙️ Forcing release of Port 53 (stopping systemd-resolved)...${C_RESET}"
systemctl stop systemd-resolved >/dev/null 2>&1
systemctl disable systemd-resolved >/dev/null 2>&1
# Mask it so it never starts again on reboot
systemctl mask systemd-resolved >/dev/null 2>&1
chattr -i /etc/resolv.conf &>/dev/null
rm -f /etc/resolv.conf
printf 'nameserver 8.8.8.8\nnameserver 8.8.4.4\n' > /etc/resolv.conf
chattr +i /etc/resolv.conf
# ----------------------------------------------------------------
echo -e "\n${C_BLUE}🔎 Checking if port 53 (UDP) is available...${C_RESET}"
if ss -lunp | grep -q ':53\s'; then
if [[ $(ps -p $(ss -lunp | grep ':53\s' | grep -oP 'pid=\K[0-9]+') -o comm=) == "systemd-resolve" ]]; then
echo -e "${C_YELLOW}⚠️ Warning: Port 53 is in use by 'systemd-resolved'.${C_RESET}"
echo -e "${C_YELLOW}This is the system's DNS stub resolver. It must be disabled to run DNSTT.${C_RESET}"
read -p "👉 Allow the script to automatically disable it and reconfigure DNS? (y/n): " resolve_confirm
if [[ "$resolve_confirm" == "y" || "$resolve_confirm" == "Y" ]]; then
echo -e "${C_GREEN}⚙️ Stopping and disabling systemd-resolved to free port 53...${C_RESET}"
systemctl stop systemd-resolved
systemctl disable systemd-resolved
chattr -i /etc/resolv.conf &>/dev/null
rm -f /etc/resolv.conf
echo "nameserver 8.8.8.8" > /etc/resolv.conf
chattr +i /etc/resolv.conf
echo -e "${C_GREEN}✅ Port 53 has been freed and DNS set to 8.8.8.8.${C_RESET}"
else
echo -e "${C_RED}❌ Cannot proceed without freeing port 53. Aborting.${C_RESET}"
return
fi
else
check_and_free_ports "53" || return
fi
else
echo -e "${C_GREEN}✅ Port 53 (UDP) is free to use.${C_RESET}"
fi
check_and_open_firewall_port 53 udp || return
local forward_port=""
local forward_desc=""
echo -e "\n${C_BLUE}Please choose where DNSTT should forward traffic:${C_RESET}"
echo -e " ${C_GREEN}[ 1]${C_RESET} ➡️ Forward to local SSH service (port 22)"
echo -e " ${C_GREEN}[ 2]${C_RESET} ➡️ Forward to local V2Ray backend (port 8787)"
read -p "👉 Enter your choice [2]: " fwd_choice
fwd_choice=${fwd_choice:-2}
if [[ "$fwd_choice" == "1" ]]; then
forward_port="22"
forward_desc="SSH (port 22)"
echo -e "${C_GREEN}ℹ️ DNSTT will forward to SSH on 127.0.0.1:22.${C_RESET}"
elif [[ "$fwd_choice" == "2" ]]; then
forward_port="8787"
forward_desc="V2Ray (port 8787)"
echo -e "${C_GREEN}ℹ️ DNSTT will forward to V2Ray on 127.0.0.1:8787.${C_RESET}"
else
echo -e "${C_RED}❌ Invalid choice. Aborting.${C_RESET}"
return
fi
local FORWARD_TARGET="127.0.0.1:$forward_port"
local NS_DOMAIN=""
local TUNNEL_DOMAIN=""
local DNSTT_RECORDS_MANAGED="true"
local NS_SUBDOMAIN=""
local TUNNEL_SUBDOMAIN=""
local HAS_IPV6="false"
read -p "👉 Auto-generate DNS records or use custom ones? (auto/custom) [auto]: " dns_choice
dns_choice=${dns_choice:-auto}
if [[ "$dns_choice" == "custom" ]]; then
DNSTT_RECORDS_MANAGED="false"
read -p "👉 Enter your full nameserver domain (e.g., ns1.yourdomain.com): " NS_DOMAIN
if [[ -z "$NS_DOMAIN" ]]; then echo -e "\n${C_RED}❌ Nameserver domain cannot be empty. Aborting.${C_RESET}"; return; fi
read -p "👉 Enter your full tunnel domain (e.g., tun.yourdomain.com): " TUNNEL_DOMAIN
if [[ -z "$TUNNEL_DOMAIN" ]]; then echo -e "\n${C_RED}❌ Tunnel domain cannot be empty. Aborting.${C_RESET}"; return; fi
else
echo -e "\n${C_BLUE}⚙️ Configuring DNS records for DNSTT...${C_RESET}"
local SERVER_IPV4
SERVER_IPV4=$(curl -s -4 icanhazip.com)
if ! _is_valid_ipv4 "$SERVER_IPV4"; then
echo -e "\n${C_RED}❌ Error: Could not retrieve a valid public IPv4 address from icanhazip.com.${C_RESET}"
echo -e "${C_YELLOW}ℹ️ Please check your server's network connection and DNS resolver settings.${C_RESET}"
echo -e " Output received: '$SERVER_IPV4'"
return 1
fi
local SERVER_IPV6
SERVER_IPV6=$(curl -s -6 icanhazip.com --max-time 5)
local RANDOM_STR
RANDOM_STR=$(tr -dc a-z0-9 < /dev/urandom | head -c 6)
NS_SUBDOMAIN="ns-$RANDOM_STR"
TUNNEL_SUBDOMAIN="tun-$RANDOM_STR"
NS_DOMAIN="$NS_SUBDOMAIN.$DESEC_DOMAIN"
TUNNEL_DOMAIN="$TUNNEL_SUBDOMAIN.$DESEC_DOMAIN"
local API_DATA
API_DATA=$(printf '[{"subname": "%s", "type": "A", "ttl": 3600, "records": ["%s"]}, {"subname": "%s", "type": "NS", "ttl": 3600, "records": ["%s."]}]' \
"$NS_SUBDOMAIN" "$SERVER_IPV4" "$TUNNEL_SUBDOMAIN" "$NS_DOMAIN")
if [[ -n "$SERVER_IPV6" ]]; then
local aaaa_record
aaaa_record=$(printf ',{"subname": "%s", "type": "AAAA", "ttl": 3600, "records": ["%s"]}' "$NS_SUBDOMAIN" "$SERVER_IPV6")
API_DATA="${API_DATA%?}${aaaa_record}]"
HAS_IPV6="true"
fi
local CREATE_RESPONSE
CREATE_RESPONSE=$(curl -s -w "%{http_code}" -X POST "https://desec.io/api/v1/domains/$DESEC_DOMAIN/rrsets/" \
-H "Authorization: Token $DESEC_TOKEN" -H "Content-Type: application/json" \
--data "$API_DATA")
local HTTP_CODE=${CREATE_RESPONSE: -3}
local RESPONSE_BODY=${CREATE_RESPONSE:0:${#CREATE_RESPONSE}-3}
if [[ "$HTTP_CODE" -ne 201 ]]; then
echo -e "${C_RED}❌ Failed to create DNSTT records. API returned HTTP $HTTP_CODE.${C_RESET}"
echo "Response: $RESPONSE_BODY" | jq
return 1
fi
fi
read -p "👉 Enter MTU value (e.g., 512, 1200) or press [Enter] for default: " mtu_value
local mtu_string=""
if [[ "$mtu_value" =~ ^[0-9]+$ ]]; then
mtu_string=" -mtu $mtu_value"
echo -e "${C_GREEN}ℹ️ Using MTU: $mtu_value${C_RESET}"
else
mtu_value=""
echo -e "${C_YELLOW}ℹ️ Using default MTU.${C_RESET}"
fi
echo -e "\n${C_BLUE}📥 Downloading pre-compiled DNSTT server binary...${C_RESET}"
local arch
arch=$(uname -m)
local binary_url=""
if [[ "$arch" == "x86_64" ]]; then
binary_url="https://dnstt.network/dnstt-server-linux-amd64"
echo -e "${C_BLUE}ℹ️ Detected x86_64 (amd64) architecture.${C_RESET}"
elif [[ "$arch" == "aarch64" || "$arch" == "arm64" ]]; then
binary_url="https://dnstt.network/dnstt-server-linux-arm64"
echo -e "${C_BLUE}ℹ️ Detected ARM64 architecture.${C_RESET}"
else
echo -e "\n${C_RED}❌ Unsupported architecture: $arch. Cannot install DNSTT.${C_RESET}"
return
fi
curl -sL "$binary_url" -o "$DNSTT_BINARY"
if [ $? -ne 0 ]; then
echo -e "\n${C_RED}❌ Failed to download the DNSTT binary.${C_RESET}"
return
fi
chmod +x "$DNSTT_BINARY"
echo -e "${C_BLUE}🔐 Generating cryptographic keys...${C_RESET}"
mkdir -p "$DNSTT_KEYS_DIR"
"$DNSTT_BINARY" -gen-key -privkey-file "$DNSTT_KEYS_DIR/server.key" -pubkey-file "$DNSTT_KEYS_DIR/server.pub"
if [[ ! -f "$DNSTT_KEYS_DIR/server.key" ]]; then echo -e "${C_RED}❌ Failed to generate DNSTT keys.${C_RESET}"; return; fi
local PUBLIC_KEY
PUBLIC_KEY=$(cat "$DNSTT_KEYS_DIR/server.pub")
echo -e "\n${C_BLUE}📝 Creating systemd service...${C_RESET}"
cat > "$DNSTT_SERVICE_FILE" <<-EOF
[Unit]
Description=DNSTT (DNS Tunnel) Server for $forward_desc
After=network-online.target
Wants=network-online.target
Conflicts=systemd-resolved.service
[Service]
Type=simple
User=root
ExecStartPre=/bin/bash -c 'systemctl stop systemd-resolved 2>/dev/null; systemctl mask systemd-resolved 2>/dev/null; chattr -i /etc/resolv.conf 2>/dev/null; printf "nameserver 8.8.8.8\\nnameserver 8.8.4.4\\n" > /etc/resolv.conf; chattr +i /etc/resolv.conf; sleep 1'
ExecStart=$DNSTT_BINARY -udp :53$mtu_string -privkey-file $DNSTT_KEYS_DIR/server.key $TUNNEL_DOMAIN $FORWARD_TARGET
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
echo -e "\n${C_BLUE}💾 Saving configuration and starting service...${C_RESET}"
cat > "$DNSTT_CONFIG_FILE" <<-EOF
NS_SUBDOMAIN="$NS_SUBDOMAIN"
TUNNEL_SUBDOMAIN="$TUNNEL_SUBDOMAIN"
NS_DOMAIN="$NS_DOMAIN"
TUNNEL_DOMAIN="$TUNNEL_DOMAIN"
PUBLIC_KEY="$PUBLIC_KEY"
FORWARD_DESC="$forward_desc"
DNSTT_RECORDS_MANAGED="$DNSTT_RECORDS_MANAGED"
HAS_IPV6="$HAS_IPV6"
MTU_VALUE="$mtu_value"
EOF
systemctl daemon-reload
systemctl enable dnstt.service
systemctl start dnstt.service
sleep 2
if systemctl is-active --quiet dnstt.service; then
echo -e "\n${C_GREEN}✅ SUCCESS: DNSTT has been installed and started!${C_RESET}"
show_dnstt_details
else
echo -e "\n${C_RED}❌ ERROR: DNSTT service failed to start.${C_RESET}"
journalctl -u dnstt.service -n 15 --no-pager
fi
}
uninstall_dnstt() {
echo -e "\n${C_BOLD}${C_PURPLE}--- 🗑️ Uninstalling DNSTT ---${C_RESET}"
if [ ! -f "$DNSTT_SERVICE_FILE" ]; then
echo -e "${C_YELLOW}ℹ️ DNSTT does not appear to be installed, skipping.${C_RESET}"
return
fi
local confirm="y"
if [[ "$UNINSTALL_MODE" != "silent" ]]; then
read -p "👉 Are you sure you want to uninstall DNSTT? This will delete DNS records if they were auto-generated. (y/n): " confirm
fi
if [[ "$confirm" != "y" ]]; then
echo -e "\n${C_YELLOW}❌ Uninstallation cancelled.${C_RESET}"
return
fi
echo -e "${C_BLUE}🛑 Stopping and disabling DNSTT service...${C_RESET}"
systemctl stop dnstt.service > /dev/null 2>&1
systemctl disable dnstt.service > /dev/null 2>&1
if [ -f "$DNSTT_CONFIG_FILE" ]; then
source "$DNSTT_CONFIG_FILE"
if [[ "$DNSTT_RECORDS_MANAGED" == "true" ]]; then
echo -e "${C_BLUE}🗑️ Removing auto-generated DNS records...${C_RESET}"
curl -s -X DELETE "https://desec.io/api/v1/domains/$DESEC_DOMAIN/rrsets/$TUNNEL_SUBDOMAIN/NS/" \
-H "Authorization: Token $DESEC_TOKEN" > /dev/null
curl -s -X DELETE "https://desec.io/api/v1/domains/$DESEC_DOMAIN/rrsets/$NS_SUBDOMAIN/A/" \
-H "Authorization: Token $DESEC_TOKEN" > /dev/null
if [[ "$HAS_IPV6" == "true" ]]; then
curl -s -X DELETE "https://desec.io/api/v1/domains/$DESEC_DOMAIN/rrsets/$NS_SUBDOMAIN/AAAA/" \
-H "Authorization: Token $DESEC_TOKEN" > /dev/null
fi
echo -e "${C_GREEN}✅ DNS records have been removed.${C_RESET}"
else
echo -e "${C_YELLOW}⚠️ DNS records were manually configured. Please delete them from your DNS provider.${C_RESET}"
fi
fi
echo -e "${C_BLUE}🗑️ Removing service files and binaries...${C_RESET}"
rm -f "$DNSTT_SERVICE_FILE"
rm -f "$DNSTT_BINARY"
rm -rf "$DNSTT_KEYS_DIR"
rm -f "$DNSTT_CONFIG_FILE"
systemctl daemon-reload
echo -e "${C_YELLOW}ℹ️ Restoring system DNS resolver...${C_RESET}"
chattr -i /etc/resolv.conf &>/dev/null
systemctl unmask systemd-resolved &>/dev/null
systemctl enable systemd-resolved &>/dev/null
systemctl start systemd-resolved &>/dev/null
echo -e "\n${C_GREEN}✅ DNSTT has been successfully uninstalled.${C_RESET}"
}
install_falcon_proxy() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🦅 Installing Falcon Proxy (Websockets/Socks) ---${C_RESET}"
if [ -f "$FALCONPROXY_SERVICE_FILE" ]; then
echo -e "\n${C_YELLOW}ℹ️ Falcon Proxy is already installed.${C_RESET}"
if [ -f "$FALCONPROXY_CONFIG_FILE" ]; then
source "$FALCONPROXY_CONFIG_FILE"
echo -e " It is configured to run on port(s): ${C_YELLOW}$PORTS${C_RESET}"
echo -e " Installed Version: ${C_YELLOW}${INSTALLED_VERSION:-Unknown}${C_RESET}"
fi
read -p "👉 Do you want to reinstall/update? (y/n): " confirm_reinstall
if [[ "$confirm_reinstall" != "y" ]]; then return; fi
fi
# Falcon Proxy ships inside the local bundle; there is no remote release lookup.
local SELECTED_VERSION="v0-websockets"
if [[ ! -f "$FF_BUNDLE_DIR/release/falconproxy" && ! -f "$FF_BUNDLE_DIR/release/falconproxyarm" ]]; then
ff_require_bundle_file "$FF_BUNDLE_DIR/release/falconproxy"
return
fi
echo -e "${C_BLUE}ℹ️ Using bundled release $SELECTED_VERSION.${C_RESET}"
local ports
read -p "👉 Enter port(s) for Falcon Proxy (e.g., 8080 or 8080 8888) [8080]: " ports
ports=${ports:-8080}
local port_array=($ports)
for port in "${port_array[@]}"; do
if ! [[ "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
echo -e "\n${C_RED}❌ Invalid port number: $port. Aborting.${C_RESET}"
return
fi
check_and_free_ports "$port" || return
check_and_open_firewall_port "$port" tcp || return
done
echo -e "\n${C_GREEN}⚙️ Detecting system architecture...${C_RESET}"
local arch=$(uname -m)
local binary_name=""
if [[ "$arch" == "x86_64" ]]; then
binary_name="falconproxy"
echo -e "${C_BLUE}ℹ️ Detected x86_64 (amd64) architecture.${C_RESET}"
elif [[ "$arch" == "aarch64" || "$arch" == "arm64" ]]; then
binary_name="falconproxyarm"
echo -e "${C_BLUE}ℹ️ Detected ARM64 architecture.${C_RESET}"
else
echo -e "\n${C_RED}❌ Unsupported architecture: $arch. Cannot install Falcon Proxy.${C_RESET}"
return
fi
echo -e "\n${C_GREEN}📥 Installing Falcon Proxy $SELECTED_VERSION ($binary_name) from local bundle...${C_RESET}"
ff_require_bundle_file "$FF_BUNDLE_DIR/release/$binary_name" || return
cp "$FF_BUNDLE_DIR/release/$binary_name" "$FALCONPROXY_BINARY"
if [ ! -s "$FALCONPROXY_BINARY" ]; then
echo -e "\n${C_RED}❌ Failed to install the bundled binary '$binary_name'.${C_RESET}"
return
fi
chmod +x "$FALCONPROXY_BINARY"
echo -e "\n${C_GREEN}📝 Creating systemd service file...${C_RESET}"
cat > "$FALCONPROXY_SERVICE_FILE" < "$FALCONPROXY_CONFIG_FILE" </dev/null 2>&1
systemctl disable falconproxy.service >/dev/null 2>&1
echo -e "${C_GREEN}🗑️ Removing service file...${C_RESET}"
rm -f "$FALCONPROXY_SERVICE_FILE"
systemctl daemon-reload
echo -e "${C_GREEN}🗑️ Removing binary and config files...${C_RESET}"
rm -f "$FALCONPROXY_BINARY"
rm -f "$FALCONPROXY_CONFIG_FILE"
echo -e "${C_GREEN}✅ Falcon Proxy has been uninstalled successfully.${C_RESET}"
}
# --- ZiVPN Installation Logic ---
install_zivpn() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🚀 Installing ZiVPN (UDP/VPN) ---${C_RESET}"
if [ -f "$ZIVPN_SERVICE_FILE" ]; then
echo -e "\n${C_YELLOW}ℹ️ ZiVPN is already installed.${C_RESET}"
return
fi
if [ ! -f "$BADVPN_SERVICE_FILE" ]; then
echo -e "\n${C_YELLOW}⚠️ ZiVPN requires the badvpn (udpgw) backend to provide internet access.${C_RESET}"
echo -e "${C_GREEN}📦 Automatically installing badvpn backend...${C_RESET}"
sleep 2
install_badvpn
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🚀 Resuming ZiVPN Installation ---${C_RESET}"
fi
check_and_free_ports 5667 || return
check_and_open_firewall_port 5667 udp || return
check_and_open_firewall_port_range "6000:19999" udp || return
echo -e "\n${C_GREEN}⚙️ Checking system architecture...${C_RESET}"
local arch=$(uname -m)
local zivpn_url=""
if [[ "$arch" == "x86_64" ]]; then
zivpn_url="https://github.com/zahidbd2/udp-zivpn/releases/download/udp-zivpn_1.4.9/udp-zivpn-linux-amd64"
echo -e "${C_BLUE}ℹ️ Detected AMD64/x86_64 architecture.${C_RESET}"
elif [[ "$arch" == "aarch64" ]]; then
zivpn_url="https://github.com/zahidbd2/udp-zivpn/releases/download/udp-zivpn_1.4.9/udp-zivpn-linux-arm64"
echo -e "${C_BLUE}ℹ️ Detected ARM64 architecture.${C_RESET}"
elif [[ "$arch" == "armv7l" || "$arch" == "arm" ]]; then
zivpn_url="https://github.com/zahidbd2/udp-zivpn/releases/download/udp-zivpn_1.4.9/udp-zivpn-linux-arm"
echo -e "${C_BLUE}ℹ️ Detected ARM architecture.${C_RESET}"
else
echo -e "${C_RED}❌ Unsupported architecture: $arch${C_RESET}"
return
fi
echo -e "\n${C_GREEN}📦 Downloading ZiVPN binary...${C_RESET}"
if ! wget -q --show-progress -O "$ZIVPN_BIN" "$zivpn_url"; then
echo -e "${C_RED}❌ Download failed. Check internet connection.${C_RESET}"
return
fi
chmod +x "$ZIVPN_BIN"
echo -e "\n${C_GREEN}⚙️ Configuring ZIVPN...${C_RESET}"
mkdir -p "$ZIVPN_DIR"
# Generate Certificates
echo -e "${C_BLUE}🔐 Generating self-signed certificates...${C_RESET}"
if ! command -v openssl &>/dev/null; then
ff_pkg_install openssl >/dev/null 2>&1 || {
echo -e "${C_RED}❌ Failed to install openssl for ZiVPN certificate generation.${C_RESET}"
return
}
fi
openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 \
-subj "/C=US/ST=California/L=Los Angeles/O=Example Corp/OU=IT Department/CN=zivpn" \
-keyout "$ZIVPN_KEY_FILE" -out "$ZIVPN_CERT_FILE" 2>/dev/null
if [ ! -f "$ZIVPN_CERT_FILE" ]; then
echo -e "${C_RED}❌ Failed to generate certificates.${C_RESET}"
return
fi
# System Tuning
echo -e "${C_BLUE}🔧 Tuning system network parameters...${C_RESET}"
sysctl -w net.core.rmem_max=16777216 >/dev/null
sysctl -w net.core.wmem_max=16777216 >/dev/null
# Create Service
echo -e "${C_BLUE}📝 Creating systemd service file...${C_RESET}"
cat < "$ZIVPN_SERVICE_FILE"
[Unit]
Description=zivpn VPN Server
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=$ZIVPN_DIR
ExecStart=$ZIVPN_BIN server -c $ZIVPN_CONFIG_FILE
Restart=always
RestartSec=3
Environment=ZIVPN_LOG_LEVEL=info
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
EOF
# Configure Passwords
echo -e "\n${C_YELLOW}🔑 ZiVPN Password Setup${C_RESET}"
read -p "👉 Enter passwords separated by commas (e.g., user1,user2) [Default: 'zi']: " input_config
if [ -n "$input_config" ]; then
IFS=',' read -r -a config_array <<< "$input_config"
# Ensure array format for JSON
json_passwords=$(printf '"%s",' "${config_array[@]}")
json_passwords="[${json_passwords%,}]"
else
json_passwords='["zi"]'
fi
# Create Config File
cat < "$ZIVPN_CONFIG_FILE"
{
"listen": ":5667",
"cert": "$ZIVPN_CERT_FILE",
"key": "$ZIVPN_KEY_FILE",
"obfs":"zivpn",
"auth": {
"mode": "passwords",
"config": $json_passwords
}
}
EOF
echo -e "\n${C_GREEN}🚀 Starting ZiVPN Service...${C_RESET}"
systemctl daemon-reload
systemctl enable zivpn.service
systemctl start zivpn.service
# Port Forwarding / Firewall
echo -e "${C_BLUE}🔥 Configuring Firewall Rules (Redirecting 6000-19999 -> 5667)...${C_RESET}"
# Determine primary interface
local iface=$(ip -4 route ls | grep default | grep -Po '(?<=dev )(\S+)' | head -1)
if [ -n "$iface" ]; then
iptables -t nat -C PREROUTING -i "$iface" -p udp --dport 6000:19999 -j DNAT --to-destination :5667 2>/dev/null || \
iptables -t nat -A PREROUTING -i "$iface" -p udp --dport 6000:19999 -j DNAT --to-destination :5667
# Note: IPTables rules are not persistent by default without iptables-persistent package
else
echo -e "${C_YELLOW}⚠️ Could not detect default interface for IPTables redirection.${C_RESET}"
fi
# Cleanup
rm -f zi.sh zi2.sh 2>/dev/null
if systemctl is-active --quiet zivpn.service; then
echo -e "\n${C_GREEN}✅ ZiVPN Installed Successfully!${C_RESET}"
echo -e " - UDP Port: 5667 (Direct)"
echo -e " - UDP Ports: 6000-19999 (Forwarded)"
else
echo -e "\n${C_RED}❌ ZiVPN Service failed to start. Check logs: journalctl -u zivpn.service${C_RESET}"
fi
}
uninstall_zivpn() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🗑️ Uninstall ZiVPN ---${C_RESET}"
if [ ! -f "$ZIVPN_SERVICE_FILE" ] && [ ! -f "$ZIVPN_BIN" ]; then
echo -e "\n${C_YELLOW}ℹ️ ZiVPN does not appear to be installed.${C_RESET}"
return
fi
read -p "👉 Are you sure you want to uninstall ZiVPN? (y/n): " confirm
if [[ "$confirm" != "y" ]]; then echo -e "${C_YELLOW}Cancelled.${C_RESET}"; return; fi
echo -e "\n${C_BLUE}🛑 Stopping services...${C_RESET}"
systemctl stop zivpn.service 2>/dev/null
systemctl disable zivpn.service 2>/dev/null
local iface
iface=$(ip -4 route ls | grep default | grep -Po '(?<=dev )(\S+)' | head -1)
if [ -n "$iface" ]; then
iptables -t nat -D PREROUTING -i "$iface" -p udp --dport 6000:19999 -j DNAT --to-destination :5667 2>/dev/null || true
fi
echo -e "${C_BLUE}🗑️ Removing files...${C_RESET}"
rm -f "$ZIVPN_SERVICE_FILE"
rm -rf "$ZIVPN_DIR"
rm -f "$ZIVPN_BIN"
systemctl daemon-reload
# Clean cache (from original uninstall script logic)
echo -e "${C_BLUE}🧹 Cleaning memory cache...${C_RESET}"
sync; echo 3 > /proc/sys/vm/drop_caches
echo -e "\n${C_GREEN}✅ ZiVPN Uninstalled Successfully.${C_RESET}"
}
purge_nginx() {
local mode="$1"
if [[ "$mode" != "silent" ]]; then
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🔥 Purge Internal Nginx Proxy ---${C_RESET}"
if ! command -v nginx &> /dev/null; then
rm -f "$NGINX_PORTS_FILE"
echo -e "\n${C_YELLOW}ℹ️ Nginx is not installed. Nothing to do.${C_RESET}"
return
fi
echo -e "\n${C_YELLOW}⚠️ This removes the internal Nginx proxy on ${NGINX_INTERNAL_HTTP_PORT}/${NGINX_INTERNAL_TLS_PORT}.${C_RESET}"
if systemctl is-active --quiet haproxy; then
echo -e "${C_YELLOW}⚠️ HAProxy will stay installed, but web payload routing from ${EDGE_PUBLIC_HTTP_PORT}/${EDGE_PUBLIC_TLS_PORT} will stop until you reinstall the stack.${C_RESET}"
fi
read -p "👉 Continue and purge Nginx? (y/n): " confirm
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
echo -e "\n${C_YELLOW}❌ Uninstallation cancelled.${C_RESET}"
return
fi
fi
echo -e "\n${C_BLUE}🛑 Stopping Nginx service...${C_RESET}"
systemctl stop nginx >/dev/null 2>&1
systemctl disable nginx >/dev/null 2>&1
echo -e "\n${C_BLUE}🗑️ Purging Nginx packages...${C_RESET}"
ff_pkg_purge nginx nginx-common >/dev/null 2>&1
ff_pkg_autoremove
echo -e "\n${C_BLUE}🗑️ Removing leftover files...${C_RESET}"
rm -f /etc/ssl/certs/nginx-selfsigned.pem
rm -f /etc/ssl/private/nginx-selfsigned.key
rm -rf /etc/nginx
rm -f "${NGINX_CONFIG_FILE}.bak"
rm -f "${NGINX_CONFIG_FILE}.bak.certbot"
rm -f "${NGINX_CONFIG_FILE}.bak.selfsigned"
rm -f "${NGINX_CONFIG_FILE}.bak.firewallfalcon"
rm -f "$NGINX_PORTS_FILE"
if [[ "$mode" != "silent" ]]; then
echo -e "\n${C_GREEN}✅ Internal Nginx proxy purged. Shared FirewallFalcon certificates were kept.${C_RESET}"
fi
}
install_nginx_proxy() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🚀 Reconfiguring Internal Nginx Proxy (8880/8443) ---${C_RESET}"
echo -e "\n${C_CYAN}This keeps HAProxy on ${EDGE_PUBLIC_HTTP_PORT}/${EDGE_PUBLIC_TLS_PORT} and rewrites the internal Nginx proxy on ${NGINX_INTERNAL_HTTP_PORT}/${NGINX_INTERNAL_TLS_PORT}.${C_RESET}"
if [ ! -s "$SSL_CERT_FILE" ] || [ ! -s "$SSL_CERT_CHAIN_FILE" ] || [ ! -s "$SSL_CERT_KEY_FILE" ]; then
echo -e "\n${C_YELLOW}⚠️ No shared FirewallFalcon certificate was found.${C_RESET}"
echo -e "${C_DIM}Running the full HAProxy edge installer so the certificate and both services stay aligned.${C_RESET}"
install_ssl_tunnel
return
fi
mkdir -p "$DB_DIR" "$SSL_CERT_DIR"
ensure_edge_stack_packages || return
systemctl stop haproxy >/dev/null 2>&1
systemctl stop nginx >/dev/null 2>&1
sleep 1
check_and_free_ports \
"$EDGE_PUBLIC_HTTP_PORT" \
"$EDGE_PUBLIC_TLS_PORT" \
"$NGINX_INTERNAL_HTTP_PORT" \
"$NGINX_INTERNAL_TLS_PORT" \
"$HAPROXY_INTERNAL_DECRYPT_PORT" || return
check_and_open_firewall_port "$EDGE_PUBLIC_HTTP_PORT" tcp || return
check_and_open_firewall_port "$EDGE_PUBLIC_TLS_PORT" tcp || return
load_edge_cert_info
local server_name="${EDGE_DOMAIN:-$(detect_preferred_host)}"
[[ -z "$server_name" ]] && server_name="_"
configure_edge_stack "$server_name" || return
echo -e "\n${C_GREEN}✅ Internal Nginx proxy reconfigured successfully.${C_RESET}"
echo -e " • Public HAProxy edge: ${C_YELLOW}${EDGE_PUBLIC_HTTP_PORT}/${EDGE_PUBLIC_TLS_PORT}${C_RESET}"
echo -e " • Internal Nginx: ${C_YELLOW}${NGINX_INTERNAL_HTTP_PORT}/${NGINX_INTERNAL_TLS_PORT}${C_RESET}"
}
request_certbot_ssl() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🔒 Shared Certbot Certificate (HAProxy + Nginx) ---${C_RESET}"
echo -e "\n${C_DIM}This will replace the shared certificate used by HAProxy on ${EDGE_PUBLIC_TLS_PORT} and internal Nginx on ${NGINX_INTERNAL_TLS_PORT}.${C_RESET}"
mkdir -p "$DB_DIR" "$SSL_CERT_DIR"
ensure_edge_stack_packages || return
load_edge_cert_info
local preferred_host
local default_domain=""
local domain_name
local email
preferred_host=$(detect_preferred_host)
if [[ -n "$EDGE_DOMAIN" ]] && ! _is_valid_ipv4 "$EDGE_DOMAIN"; then
default_domain="$EDGE_DOMAIN"
elif [[ -n "$preferred_host" ]] && ! _is_valid_ipv4 "$preferred_host"; then
default_domain="$preferred_host"
fi
if [[ -n "$default_domain" ]]; then
read -p "👉 Enter your domain name [$default_domain]: " domain_name
domain_name=${domain_name:-$default_domain}
else
read -p "👉 Enter your domain name (e.g. vpn.example.com): " domain_name
fi
if [[ -z "$domain_name" ]]; then
echo -e "\n${C_RED}❌ Domain name cannot be empty.${C_RESET}"
return
fi
if _is_valid_ipv4 "$domain_name"; then
echo -e "\n${C_RED}❌ Certbot requires a real domain name, not a raw IP address.${C_RESET}"
return
fi
read -p "👉 Enter your email for Let's Encrypt [${EDGE_EMAIL}]: " email
email=${email:-$EDGE_EMAIL}
if [[ -z "$email" ]]; then
echo -e "\n${C_RED}❌ Email address cannot be empty.${C_RESET}"
return
fi
check_and_open_firewall_port "$EDGE_PUBLIC_HTTP_PORT" tcp || return
check_and_open_firewall_port "$EDGE_PUBLIC_TLS_PORT" tcp || return
obtain_certbot_edge_cert "$domain_name" "$email" || return
configure_edge_stack "$domain_name" || return
echo -e "\n${C_GREEN}✅ Shared Certbot certificate applied successfully.${C_RESET}"
echo -e " • Domain: ${C_YELLOW}${domain_name}${C_RESET}"
echo -e " • Public edge: ${C_YELLOW}${EDGE_PUBLIC_HTTP_PORT}/${EDGE_PUBLIC_TLS_PORT}${C_RESET}"
}
nginx_proxy_menu() {
while true; do
show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🌐 Internal Nginx Proxy Management ---${C_RESET}"
local nginx_status="${C_STATUS_I}Inactive${C_RESET}"
local haproxy_status="${C_STATUS_I}Inactive${C_RESET}"
if systemctl is-active --quiet nginx; then
nginx_status="${C_STATUS_A}Active${C_RESET}"
fi
if systemctl is-active --quiet haproxy; then
haproxy_status="${C_STATUS_A}Active${C_RESET}"
fi
load_edge_cert_info
local cert_info="${EDGE_CERT_MODE:-Not configured}"
if [[ -n "$EDGE_DOMAIN" ]]; then
cert_info="${cert_info} - ${EDGE_DOMAIN}"
fi
echo -e "\n${C_WHITE}Nginx:${C_RESET} ${nginx_status}"
echo -e "${C_WHITE}HAProxy:${C_RESET} ${haproxy_status}"
echo -e "${C_DIM}Public Edge: ${EDGE_PUBLIC_HTTP_PORT}/${EDGE_PUBLIC_TLS_PORT} | Internal Nginx: ${NGINX_INTERNAL_HTTP_PORT}/${NGINX_INTERNAL_TLS_PORT}${C_RESET}"
echo -e "${C_DIM}Shared Certificate: ${cert_info}${C_RESET}"
echo -e "\n${C_BOLD}Select an action:${C_RESET}\n"
if systemctl is-active --quiet nginx; then
printf " ${C_CHOICE}[ 1]${C_RESET} %-40s\n" "🛑 Stop Nginx Service"
printf " ${C_CHOICE}[ 2]${C_RESET} %-40s\n" "🔄 Restart HAProxy + Nginx Stack"
printf " ${C_CHOICE}[ 3]${C_RESET} %-40s\n" "⚙️ Re-install/Re-configure Edge Stack"
printf " ${C_CHOICE}[ 4]${C_RESET} %-40s\n" "🔒 Switch/Renew Shared SSL (Certbot)"
printf " ${C_CHOICE}[ 5]${C_RESET} %-40s\n" "🔥 Uninstall/Purge Nginx"
else
printf " ${C_CHOICE}[ 1]${C_RESET} %-40s\n" "▶️ Start Nginx Service"
printf " ${C_CHOICE}[ 3]${C_RESET} %-40s\n" "⚙️ Install/Configure Edge Stack"
printf " ${C_CHOICE}[ 4]${C_RESET} %-40s\n" "🔒 Switch/Renew Shared SSL (Certbot)"
printf " ${C_CHOICE}[ 5]${C_RESET} %-40s\n" "🔥 Uninstall/Purge Nginx"
fi
echo -e "\n ${C_WARN}[ 0]${C_RESET} ↩️ Return"
echo
if ! read -r -p "$(echo -e ${C_PROMPT}"👉 Select an option: "${C_RESET})" choice; then
echo
return
fi
case $choice in
1)
if systemctl is-active --quiet nginx; then
echo -e "\n${C_BLUE}🛑 Stopping Nginx...${C_RESET}"
systemctl stop nginx
echo -e "${C_GREEN}✅ Nginx stopped.${C_RESET}"
if systemctl is-active --quiet haproxy; then
echo -e "${C_YELLOW}⚠️ HAProxy is still running, but web traffic that depends on internal Nginx will not work until Nginx starts again.${C_RESET}"
fi
else
echo -e "\n${C_BLUE}▶️ Starting Nginx...${C_RESET}"
systemctl start nginx
if systemctl is-active --quiet nginx; then
echo -e "${C_GREEN}✅ Nginx started.${C_RESET}"
else
echo -e "${C_RED}❌ Failed to start Nginx.${C_RESET}"
fi
fi
press_enter
;;
2)
echo -e "\n${C_BLUE}🔄 Restarting Nginx and HAProxy...${C_RESET}"
local restart_ok=true
systemctl restart nginx || restart_ok=false
if command -v haproxy &> /dev/null; then
systemctl restart haproxy || restart_ok=false
else
restart_ok=false
fi
if $restart_ok && systemctl is-active --quiet nginx && systemctl is-active --quiet haproxy; then
echo -e "${C_GREEN}✅ HAProxy + Nginx stack restarted.${C_RESET}"
else
echo -e "${C_RED}❌ One or more services failed to restart.${C_RESET}"
fi
press_enter
;;
3)
install_nginx_proxy; press_enter
;;
4)
request_certbot_ssl; press_enter
;;
5)
purge_nginx; press_enter
;;
0) return ;;
*) invalid_option ;;
esac
done
}
install_panel_menu() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 💻 Install X-UI / 3X-UI Panel ---${C_RESET}"
echo -e "\n${C_CYAN}Select which panel to install:${C_RESET}\n"
printf " ${C_CHOICE}[ 1]${C_RESET} %-45s %s\n" "🚀 3X-UI Panel (MHSanaei)" "${C_STATUS_A}⭐ Default${C_RESET}"
printf " ${C_CHOICE}[ 2]${C_RESET} %-45s %s\n" "📦 X-UI Panel (alireza0)" ""
echo -e "\n ${C_RED}[ 0]${C_RESET} ❌ Cancel"
echo
read -p "👉 Select panel [1]: " panel_choice
panel_choice=${panel_choice:-1}
case $panel_choice in
1) install_3xui_panel ;;
2) install_xui_panel ;;
0) echo -e "\n${C_YELLOW}❌ Installation cancelled.${C_RESET}" ;;
*) echo -e "\n${C_RED}❌ Invalid option.${C_RESET}" ;;
esac
}
install_3xui_panel() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🚀 Install 3X-UI Panel ---${C_RESET}"
echo -e "\nThis will download and run the official installation script for 3X-UI (MHSanaei)."
echo -e "Choose an installation option:\n"
printf " ${C_GREEN}[ 1]${C_RESET} %-40s\n" "Install the latest version of 3X-UI"
printf " ${C_GREEN}[ 2]${C_RESET} %-40s\n" "Install a specific version of 3X-UI"
echo -e "\n ${C_RED}[ 0]${C_RESET} ❌ Cancel Installation"
echo
read -p "👉 Select an option: " choice
case $choice in
1)
echo -e "\n${C_BLUE}⚙️ Installing the latest version...${C_RESET}"
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
;;
2)
read -p "👉 Enter the version to install (e.g., 2.4.5): " version
if [[ -z "$version" ]]; then
echo -e "\n${C_RED}❌ Version number cannot be empty.${C_RESET}"
return
fi
echo -e "\n${C_BLUE}⚙️ Installing version ${C_YELLOW}$version...${C_RESET}"
bash <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/v$version/install.sh") "v$version"
;;
0)
echo -e "\n${C_YELLOW}❌ Installation cancelled.${C_RESET}"
;;
*)
echo -e "\n${C_RED}❌ Invalid option.${C_RESET}"
;;
esac
}
install_xui_panel() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 📦 Install X-UI Panel (Legacy) ---${C_RESET}"
echo -e "\nThis will download and run the installation script for X-UI (alireza0)."
echo -e "Choose an installation option:\n"
printf " ${C_GREEN}[ 1]${C_RESET} %-40s\n" "Install the latest version of X-UI"
printf " ${C_GREEN}[ 2]${C_RESET} %-40s\n" "Install a specific version of X-UI"
echo -e "\n ${C_RED}[ 0]${C_RESET} ❌ Cancel Installation"
echo
read -p "👉 Select an option: " choice
case $choice in
1)
echo -e "\n${C_BLUE}⚙️ Installing the latest version...${C_RESET}"
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh)
;;
2)
read -p "👉 Enter the version to install (e.g., 1.8.0): " version
if [[ -z "$version" ]]; then
echo -e "\n${C_RED}❌ Version number cannot be empty.${C_RESET}"
return
fi
echo -e "\n${C_BLUE}⚙️ Installing version ${C_YELLOW}$version...${C_RESET}"
VERSION=$version bash <(curl -Ls "https://raw.githubusercontent.com/alireza0/x-ui/$version/install.sh") "$version"
;;
0)
echo -e "\n${C_YELLOW}❌ Installation cancelled.${C_RESET}"
;;
*)
echo -e "\n${C_RED}❌ Invalid option.${C_RESET}"
;;
esac
}
uninstall_xui_panel() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🗑️ Uninstall X-UI / 3X-UI Panel ---${C_RESET}"
if ! command -v x-ui &> /dev/null; then
echo -e "\n${C_YELLOW}ℹ️ No X-UI/3X-UI panel appears to be installed.${C_RESET}"
return
fi
read -p "👉 Are you sure you want to thoroughly uninstall X-UI/3X-UI? (y/n): " confirm
if [[ "$confirm" == "y" ]]; then
echo -e "\n${C_BLUE}⚙️ Running the default uninstaller first...${C_RESET}"
x-ui uninstall >/dev/null 2>&1
echo -e "\n${C_BLUE}🧹 Performing a full cleanup to ensure complete removal...${C_RESET}"
echo " - Stopping and disabling x-ui service..."
systemctl stop x-ui >/dev/null 2>&1
systemctl disable x-ui >/dev/null 2>&1
echo " - Removing x-ui files and directories..."
rm -f /etc/systemd/system/x-ui.service
rm -f /usr/local/bin/x-ui
rm -rf /usr/local/x-ui/
rm -rf /etc/x-ui/
echo " - Reloading systemd daemon..."
systemctl daemon-reload
echo -e "\n${C_GREEN}✅ X-UI/3X-UI has been thoroughly uninstalled.${C_RESET}"
else
echo -e "\n${C_YELLOW}❌ Uninstallation cancelled.${C_RESET}"
fi
}
refresh_ssh_session_cache() {
local now db_mtime
printf -v now '%(%s)T' -1
db_mtime=$(stat -c %Y "$DB_FILE" 2>/dev/null || echo 0)
if (( SSH_SESSION_CACHE_TS > 0 && now - SSH_SESSION_CACHE_TS < SSH_SESSION_CACHE_TTL && db_mtime == SSH_SESSION_CACHE_DB_MTIME )); then
return
fi
SSH_SESSION_COUNTS=()
SSH_SESSION_PIDS=()
SSH_SESSION_TOTAL=0
SSH_SESSION_CACHE_DB_MTIME=$db_mtime
if [[ ! -s "$DB_FILE" ]]; then
SSH_SESSION_CACHE_TS=$now
return
fi
local -A managed_user_lookup=()
local -A uid_user_lookup=()
local -A session_pids=()
local -A loginuid_pids=()
local managed_user system_user system_uid ssh_pid ssh_owner candidate_user login_uid
while IFS=: read -r managed_user _rest; do
[[ -n "$managed_user" && "$managed_user" != \#* ]] && managed_user_lookup["$managed_user"]=1
done < "$DB_FILE"
while IFS=: read -r system_user _ system_uid _rest; do
[[ -n "$system_user" && "$system_uid" =~ ^[0-9]+$ ]] && uid_user_lookup["$system_uid"]="$system_user"
done < /etc/passwd
while read -r ssh_pid ssh_owner; do
[[ "$ssh_pid" =~ ^[0-9]+$ ]] || continue
# Method 1: process owner matches a managed user directly
if [[ -n "$ssh_owner" && "$ssh_owner" != "root" && "$ssh_owner" != "sshd" && -n "${managed_user_lookup[$ssh_owner]+x}" ]]; then
session_pids["$ssh_owner"]+="$ssh_pid "
fi
done < <(ps -C sshd,sshd-session -o pid=,user= 2>/dev/null)
# Method 2: kernel loginuid with comm/PPid validation (more robust — matches limiter logic)
local p pid_dir pid_num comm ppid_val session_user
for p in /proc/[0-9]*/loginuid; do
[[ -f "$p" ]] || continue
login_uid=""
read -r login_uid < "$p" || login_uid=""
[[ "$login_uid" =~ ^[0-9]+$ && "$login_uid" != "4294967295" ]] || continue
candidate_user="${uid_user_lookup[$login_uid]}"
[[ -n "$candidate_user" && -n "${managed_user_lookup[$candidate_user]+x}" ]] || continue
pid_dir=$(dirname "$p")
pid_num=$(basename "$pid_dir")
comm=""
read -r comm < "$pid_dir/comm" 2>/dev/null || comm=""
[[ "$comm" == "sshd" || "$comm" == "sshd-session" ]] || continue
# Filter out the master sshd process (PPid=1)
ppid_val=""
while read -r key value; do
[[ "$key" == "PPid:" ]] && { ppid_val="$value"; break; }
done < "$pid_dir/status" 2>/dev/null
[[ "$ppid_val" == "1" ]] && continue
loginuid_pids["$candidate_user"]+="$pid_num "
done
local user pid
for user in "${!managed_user_lookup[@]}"; do
# CRITICAL: unset before declare to reset per-user (bash declare is function-scoped)
unset unique_pids
local -A unique_pids=()
# Use ONLY ps-based session_pids for accurate counting.
# loginuid_pids can double-count (root-owned sshd has user's loginuid on Ubuntu 24)
for pid in ${session_pids[$user]}; do
[[ "$pid" =~ ^[0-9]+$ ]] && unique_pids["$pid"]=1
done
SSH_SESSION_COUNTS["$user"]=${#unique_pids[@]}
if (( ${#unique_pids[@]} > 0 )); then
for pid in "${!unique_pids[@]}"; do
SSH_SESSION_PIDS["$user"]+="$pid "
done
SSH_SESSION_TOTAL=$((SSH_SESSION_TOTAL + ${#unique_pids[@]}))
fi
done
SSH_SESSION_CACHE_TS=$now
}
count_managed_online_sessions() {
refresh_ssh_session_cache
echo "$SSH_SESSION_TOTAL"
}
invalidate_banner_cache() {
BANNER_CACHE_TS=0
SSH_SESSION_CACHE_TS=0
}
refresh_banner_cache() {
local now
printf -v now '%(%s)T' -1
if (( BANNER_CACHE_TS > 0 && now - BANNER_CACHE_TS < BANNER_CACHE_TTL )); then
return
fi
if [[ -z "$BANNER_CACHE_OS_NAME" ]]; then
BANNER_CACHE_OS_NAME=$(grep -oP 'PRETTY_NAME="\K[^"]+' /etc/os-release 2>/dev/null || echo "Linux")
fi
BANNER_CACHE_UP_TIME=$(uptime -p 2>/dev/null | sed 's/up //' || echo "unknown")
BANNER_CACHE_RAM_USAGE=$(free -m | awk '/^Mem:/{if($2>0){printf "%.2f", $3*100/$2}else{print "0.00"}}')
BANNER_CACHE_CPU_LOAD=$(awk '{print $1}' /proc/loadavg 2>/dev/null)
if [[ -s "$DB_FILE" ]]; then
BANNER_CACHE_TOTAL_USERS=0
while IFS=: read -r _u _rest; do
[[ -n "$_u" && "$_u" != \#* ]] && (( BANNER_CACHE_TOTAL_USERS++ ))
done < "$DB_FILE"
else
BANNER_CACHE_TOTAL_USERS=0
fi
BANNER_CACHE_ONLINE_USERS=$(count_managed_online_sessions)
BANNER_CACHE_TS=$now
}
show_banner() {
refresh_banner_cache
[[ -t 1 ]] && clear
echo
echo -e "${C_TITLE} TNS243-GLOBAL Manager ${C_RESET}${C_DIM}| v4.0.0 Premium Edition${C_RESET}"
echo -e "${C_BLUE} ─────────────────────────────────────────────────────────${C_RESET}"
printf " ${C_GRAY}%-10s${C_RESET} %-20s ${C_GRAY}|${C_RESET} %s\n" "OS" "$BANNER_CACHE_OS_NAME" "Uptime: $BANNER_CACHE_UP_TIME"
printf " ${C_GRAY}%-10s${C_RESET} %-20s ${C_GRAY}|${C_RESET} %s\n" "Memory" "${BANNER_CACHE_RAM_USAGE}% Used" "Online Sessions: ${C_WHITE}${BANNER_CACHE_ONLINE_USERS}${C_RESET}"
printf " ${C_GRAY}%-10s${C_RESET} %-20s ${C_GRAY}|${C_RESET} %s\n" "Users" "${BANNER_CACHE_TOTAL_USERS} Managed Accounts" "Sys Load (1m): ${C_GREEN}${BANNER_CACHE_CPU_LOAD}${C_RESET}"
echo -e "${C_BLUE} ─────────────────────────────────────────────────────────${C_RESET}"
}
protocol_menu() {
while true; do
show_banner
local badvpn_status; if systemctl is-active --quiet badvpn; then badvpn_status="${C_STATUS_A}(Active)${C_RESET}"; else badvpn_status="${C_STATUS_I}(Inactive)${C_RESET}"; fi
local udp_custom_status; if systemctl is-active --quiet udp-custom; then udp_custom_status="${C_STATUS_A}(Active)${C_RESET}"; else udp_custom_status="${C_STATUS_I}(Inactive)${C_RESET}"; fi
local zivpn_status; if systemctl is-active --quiet zivpn.service; then zivpn_status="${C_STATUS_A}(Active)${C_RESET}"; else zivpn_status="${C_STATUS_I}(Inactive)${C_RESET}"; fi
local ssl_tunnel_text="HAProxy Edge Stack (80/443)"
local ssl_tunnel_status="${C_STATUS_I}(Inactive)${C_RESET}"
if systemctl is-active --quiet haproxy; then
ssl_tunnel_status="${C_STATUS_A}(Active)${C_RESET}"
fi
local dnstt_status; if systemctl is-active --quiet dnstt.service; then dnstt_status="${C_STATUS_A}(Active)${C_RESET}"; else dnstt_status="${C_STATUS_I}(Inactive)${C_RESET}"; fi
local falconproxy_status="${C_STATUS_I}(Inactive)${C_RESET}"
local falconproxy_ports=""
if systemctl is-active --quiet falconproxy; then
if [ -f "$FALCONPROXY_CONFIG_FILE" ]; then source "$FALCONPROXY_CONFIG_FILE"; fi
falconproxy_ports=" ($PORTS)"
falconproxy_status="${C_STATUS_A}(Active - ${INSTALLED_VERSION:-latest})${C_RESET}"
fi
local nginx_status; if systemctl is-active --quiet nginx; then nginx_status="${C_STATUS_A}(Active)${C_RESET}"; else nginx_status="${C_STATUS_I}(Inactive)${C_RESET}"; fi
local xui_status; if command -v x-ui &> /dev/null; then xui_status="${C_STATUS_A}(Installed)${C_RESET}"; else xui_status="${C_STATUS_I}(Not Installed)${C_RESET}"; fi # 3X-UI uses same 'x-ui' binary name
echo -e "\n ${C_TITLE}══════════════[ ${C_BOLD}🔌 PROTOCOL & PANEL MANAGEMENT ${C_RESET}${C_TITLE}]══════════════${C_RESET}"
echo -e " ${C_ACCENT}--- TUNNELLING PROTOCOLS---${C_RESET}"
printf " ${C_CHOICE}[ 1]${C_RESET} %-45s %s\n" "🚀 Install badvpn (UDP 7300)" "$badvpn_status"
printf " ${C_CHOICE}[ 2]${C_RESET} %-45s\n" "🗑️ Uninstall badvpn"
printf " ${C_CHOICE}[ 3]${C_RESET} %-45s %s\n" "🚀 Install udp-custom" "$udp_custom_status"
printf " ${C_CHOICE}[ 4]${C_RESET} %-45s\n" "🗑️ Uninstall udp-custom"
printf " ${C_CHOICE}[ 5]${C_RESET} %-45s %s\n" "🔒 Install ${ssl_tunnel_text}" "$ssl_tunnel_status"
printf " ${C_CHOICE}[ 6]${C_RESET} %-45s\n" "🗑️ Uninstall HAProxy Edge Stack"
printf " ${C_CHOICE}[ 7]${C_RESET} %-45s %s\n" "📡 Install/View DNSTT (Port 53)" "$dnstt_status"
printf " ${C_CHOICE}[ 8]${C_RESET} %-45s\n" "🗑️ Uninstall DNSTT"
printf " ${C_CHOICE}[ 9]${C_RESET} %-45s %s\n" "🦅 Install Falcon Proxy (Select Version)" "$falconproxy_status"
printf " ${C_CHOICE}[10]${C_RESET} %-45s\n" "🗑️ Uninstall Falcon Proxy"
printf " ${C_CHOICE}[11]${C_RESET} %-45s %s\n" "🌐 Install/Manage Internal Nginx (8880/8443)" "$nginx_status"
printf " ${C_CHOICE}[14]${C_RESET} %-45s %s\n" "🛡️ Install ZiVPN (UDP 5667)" "$zivpn_status"
printf " ${C_CHOICE}[15]${C_RESET} %-45s\n" "🗑️ Uninstall ZiVPN"
echo -e " ${C_ACCENT}--- 💻 MANAGEMENT PANELS ---${C_RESET}"
printf " ${C_CHOICE}[12]${C_RESET} %-45s %s\n" "💻 Install X-UI / 3X-UI Panel" "$xui_status"
printf " ${C_CHOICE}[13]${C_RESET} %-45s\n" "🗑️ Uninstall X-UI / 3X-UI Panel"
echo -e " ${C_DIM}~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~${C_RESET}"
echo -e " ${C_WARN}[ 0]${C_RESET} ↩️ Return"
echo
if ! read -r -p "$(echo -e ${C_PROMPT}"👉 Select an option: "${C_RESET})" choice; then
echo
return
fi
case $choice in
1) install_badvpn; press_enter ;; 2) uninstall_badvpn; press_enter ;;
3) install_udp_custom; press_enter ;; 4) uninstall_udp_custom; press_enter ;;
5) install_ssl_tunnel; press_enter ;; 6) uninstall_ssl_tunnel; press_enter ;;
7) install_dnstt; press_enter ;; 8) uninstall_dnstt; press_enter ;;
9) install_falcon_proxy; press_enter ;; 10) uninstall_falcon_proxy; press_enter ;;
11) nginx_proxy_menu ;;
12) install_panel_menu; press_enter ;; 13) uninstall_xui_panel; press_enter ;;
14) install_zivpn; press_enter ;; 15) uninstall_zivpn; press_enter ;;
0) return ;;
*) invalid_option ;;
esac
done
}
# ====================================================================
# --- Web Control Panel Functions ---
# ====================================================================
install_web_panel() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🌐 Installing Web Control Panel ---${C_RESET}"
if [ -f "$PANEL_SERVICE_FILE" ]; then
echo -e "\n${C_YELLOW}ℹ️ Web Panel is already installed.${C_RESET}"
show_panel_credentials
return
fi
# Check Python 3
if ! command -v python3 &>/dev/null; then
echo -e "${C_RED}❌ Python 3 is required but not installed.${C_RESET}"
echo -e "${C_YELLOW}Installing python3...${C_RESET}"
ff_pkg_install python3 || { echo -e "${C_RED}❌ Failed to install python3.${C_RESET}"; return; }
fi
echo -e "${C_BLUE}🔎 Checking if port $PANEL_PORT is available...${C_RESET}"
check_and_free_ports "$PANEL_PORT" || return
check_and_open_firewall_port "$PANEL_PORT" tcp || return
# Generate random credentials and secret URL path
local panel_user
panel_user=$(tr -dc 'a-z' < /dev/urandom | head -c 4)$(tr -dc '0-9' < /dev/urandom | head -c 4)
local panel_pass
panel_pass=$(tr -dc 'A-Za-z0-9@#$' < /dev/urandom | head -c 16)
local panel_pass_hash
panel_pass_hash=$(echo -n "$panel_pass" | sha256sum | awk '{print $1}')
local panel_secret
panel_secret="panel_$(tr -dc 'a-z0-9' < /dev/urandom | head -c 8)"
echo -e "${C_BLUE}📥 Installing panel files from local bundle...${C_RESET}"
mkdir -p "$PANEL_HTML_DIR"
# Install backend
ff_require_bundle_file "$FF_BUNDLE_DIR/panel/panel.py" || return
echo -e "${C_BLUE}ℹ️ Using local bundle copy: panel.py${C_RESET}"
cp "$FF_BUNDLE_DIR/panel/panel.py" "$PANEL_SCRIPT"
if [ ! -s "$PANEL_SCRIPT" ]; then
echo -e "${C_RED}❌ Failed to install panel backend.${C_RESET}"
return
fi
chmod +x "$PANEL_SCRIPT"
sed -i 's/\r$//' "$PANEL_SCRIPT" 2>/dev/null
# Install frontend
ff_require_bundle_file "$FF_BUNDLE_DIR/panel/index.html" || return
echo -e "${C_BLUE}ℹ️ Using local bundle copy: index.html${C_RESET}"
cp "$FF_BUNDLE_DIR/panel/index.html" "$PANEL_HTML_FILE"
if [ ! -s "$PANEL_HTML_FILE" ]; then
echo -e "${C_RED}❌ Failed to install panel frontend.${C_RESET}"
return
fi
# Save credentials
cat > "$PANEL_CONF" <<-PEOF
PANEL_USER="$panel_user"
PANEL_PASS_HASH="$panel_pass_hash"
PANEL_PASS_PLAIN="$panel_pass"
PANEL_SECRET="$panel_secret"
PEOF
chmod 600 "$PANEL_CONF"
# Create systemd service
cat > "$PANEL_SERVICE_FILE" <<-SEOF
[Unit]
Description=TNS243-GLOBAL Web Control Panel
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
ExecStart=/usr/bin/python3 $PANEL_SCRIPT
Restart=always
RestartSec=5
Nice=10
MemoryHigh=64M
MemoryMax=96M
Environment=PANEL_PORT=$PANEL_PORT
[Install]
WantedBy=multi-user.target
SEOF
systemctl daemon-reload
systemctl enable firewallfalcon-panel &>/dev/null
systemctl start firewallfalcon-panel &>/dev/null
sleep 2
if systemctl is-active --quiet firewallfalcon-panel; then
local server_ip
server_ip=$(curl -s -4 --max-time 3 icanhazip.com 2>/dev/null || echo "YOUR_SERVER_IP")
clear; show_banner
echo -e "${C_GREEN}=====================================================${C_RESET}"
echo -e "${C_GREEN} ✅ Web Control Panel Installed Successfully! ${C_RESET}"
echo -e "${C_GREEN}=====================================================${C_RESET}"
echo -e "\n${C_CYAN} 🌐 Panel URL:${C_RESET} ${C_YELLOW}http://${server_ip}:${PANEL_PORT}/${panel_secret}${C_RESET}"
echo -e "${C_CYAN} 👤 Username:${C_RESET} ${C_YELLOW}${panel_user}${C_RESET}"
echo -e "${C_CYAN} 🔑 Password:${C_RESET} ${C_YELLOW}${panel_pass}${C_RESET}"
echo -e "${C_CYAN} 🔐 Secret Path:${C_RESET} ${C_YELLOW}/${panel_secret}${C_RESET}"
echo -e "\n${C_DIM} Save these credentials! You can view them later from option [21] > [3].${C_RESET}"
else
echo -e "\n${C_RED}❌ Panel service failed to start. Checking logs:${C_RESET}"
journalctl -u firewallfalcon-panel -n 15 --no-pager
fi
}
uninstall_web_panel() {
if [ ! -f "$PANEL_SERVICE_FILE" ]; then
if [[ "$UNINSTALL_MODE" != "silent" ]]; then
echo -e "${C_YELLOW}ℹ️ Web Panel is not installed.${C_RESET}"
fi
return
fi
if [[ "$UNINSTALL_MODE" != "silent" ]]; then
echo -e "\n${C_BOLD}${C_PURPLE}--- 🗑️ Uninstalling Web Control Panel ---${C_RESET}"
read -p "👉 Are you sure you want to uninstall the Web Panel? (y/n): " confirm
if [[ "$confirm" != "y" ]]; then
echo -e "\n${C_YELLOW}❌ Uninstallation cancelled.${C_RESET}"
return
fi
fi
echo -e "${C_BLUE}🛑 Stopping and removing Web Panel service...${C_RESET}"
systemctl stop firewallfalcon-panel &>/dev/null
systemctl disable firewallfalcon-panel &>/dev/null
rm -f "$PANEL_SERVICE_FILE"
rm -f "$PANEL_SCRIPT"
rm -rf "$PANEL_HTML_DIR"
rm -f "$PANEL_CONF"
systemctl daemon-reload
echo -e "${C_GREEN}✅ Web Panel has been uninstalled.${C_RESET}"
}
show_panel_credentials() {
if [ ! -f "$PANEL_CONF" ]; then
echo -e "\n${C_YELLOW}ℹ️ Web Panel is not installed.${C_RESET}"
return
fi
source "$PANEL_CONF"
local server_ip secret_suffix
server_ip=$(curl -s -4 --max-time 3 icanhazip.com 2>/dev/null || echo "YOUR_SERVER_IP")
secret_suffix=""
if [[ -n "$PANEL_SECRET" ]]; then
secret_suffix="/${PANEL_SECRET}"
fi
echo -e "\n${C_GREEN}=====================================================${C_RESET}"
echo -e "${C_GREEN} 🌐 Web Panel Credentials ${C_RESET}"
echo -e "${C_GREEN}=====================================================${C_RESET}"
echo -e "\n${C_CYAN} 🌐 Panel URL:${C_RESET} ${C_YELLOW}http://${server_ip}:${PANEL_PORT}${secret_suffix}${C_RESET}"
echo -e "${C_CYAN} 👤 Username:${C_RESET} ${C_YELLOW}${PANEL_USER}${C_RESET}"
echo -e "${C_CYAN} 🔑 Password:${C_RESET} ${C_YELLOW}${PANEL_PASS_PLAIN}${C_RESET}"
if [[ -n "$PANEL_SECRET" ]]; then
echo -e "${C_CYAN} 🔐 Secret Path:${C_RESET} ${C_YELLOW}/${PANEL_SECRET}${C_RESET}"
fi
if systemctl is-active --quiet firewallfalcon-panel 2>/dev/null; then
echo -e "\n${C_CYAN} 📡 Status:${C_RESET} ${C_GREEN}🟢 Running${C_RESET}"
else
echo -e "\n${C_CYAN} 📡 Status:${C_RESET} ${C_RED}🔴 Stopped${C_RESET}"
fi
}
change_panel_credentials() {
if [ ! -f "$PANEL_CONF" ]; then
echo -e "\n${C_YELLOW}ℹ️ Web Panel is not installed.${C_RESET}"
return
fi
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🔑 Change Web Panel Credentials & Secret Path ---${C_RESET}"
show_panel_credentials
echo ""
read -p "👉 Enter new username (or press Enter to keep current): " new_user
read -p "🔑 Enter new password (or press Enter to auto-generate): " new_pass
read -p "🔐 Enter new secret URL path (e.g., secret123, or press Enter to keep): " new_secret
source "$PANEL_CONF"
if [[ -z "$new_user" ]]; then
new_user="$PANEL_USER"
fi
if [[ -z "$new_pass" ]]; then
new_pass=$(tr -dc 'A-Za-z0-9@#$' < /dev/urandom | head -c 16)
echo -e "${C_GREEN}🔑 Auto-generated password: ${C_YELLOW}$new_pass${C_RESET}"
fi
if [[ -z "$new_secret" ]]; then
new_secret="${PANEL_SECRET:-panel_$(tr -dc 'a-z0-9' < /dev/urandom | head -c 8)}"
fi
new_secret=$(echo "$new_secret" | sed 's/^\///')
local new_hash
new_hash=$(echo -n "$new_pass" | sha256sum | awk '{print $1}')
cat > "$PANEL_CONF" <<-PEOF
PANEL_USER="$new_user"
PANEL_PASS_HASH="$new_hash"
PANEL_PASS_PLAIN="$new_pass"
PANEL_SECRET="$new_secret"
PEOF
chmod 600 "$PANEL_CONF"
systemctl restart firewallfalcon-panel &>/dev/null
echo -e "\n${C_GREEN}✅ Panel credentials & secret path updated!${C_RESET}"
echo -e " ${C_CYAN}👤 Username:${C_RESET} ${C_YELLOW}$new_user${C_RESET}"
echo -e " ${C_CYAN}🔑 Password:${C_RESET} ${C_YELLOW}$new_pass${C_RESET}"
echo -e " ${C_CYAN}🔐 Secret Path:${C_RESET} ${C_YELLOW}/$new_secret${C_RESET}"
}
web_panel_menu() {
while true; do
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🌐 Web Control Panel ---${C_RESET}\n"
if [ -f "$PANEL_SERVICE_FILE" ]; then
if systemctl is-active --quiet firewallfalcon-panel 2>/dev/null; then
echo -e " ${C_DIM}Status: ${C_GREEN}🟢 Installed & Running${C_RESET}\n"
else
echo -e " ${C_DIM}Status: ${C_RED}🔴 Installed but Stopped${C_RESET}\n"
fi
else
echo -e " ${C_DIM}Status: ${C_YELLOW}⚪ Not Installed${C_RESET}\n"
fi
printf " ${C_GREEN}[ 1]${C_RESET} %-35s\n" "🚀 Install Web Panel"
printf " ${C_GREEN}[ 2]${C_RESET} %-35s\n" "🗑️ Uninstall Web Panel"
printf " ${C_GREEN}[ 3]${C_RESET} %-35s\n" "🔑 Show Panel Credentials"
printf " ${C_GREEN}[ 4]${C_RESET} %-35s\n" "🔄 Change Panel Credentials"
printf " ${C_GREEN}[ 5]${C_RESET} %-35s\n" "🔃 Restart Panel Service"
echo -e "\n ${C_RED}[ 0]${C_RESET} ↩️ Back to Main Menu"
echo
read -r -p "👉 Enter your choice: " panel_choice
case $panel_choice in
1) install_web_panel; press_enter ;;
2) uninstall_web_panel; press_enter ;;
3) show_panel_credentials; press_enter ;;
4) change_panel_credentials; press_enter ;;
5)
if [ -f "$PANEL_SERVICE_FILE" ]; then
systemctl restart firewallfalcon-panel &>/dev/null
sleep 1
if systemctl is-active --quiet firewallfalcon-panel; then
echo -e "\n${C_GREEN}✅ Web Panel service restarted successfully.${C_RESET}"
else
echo -e "\n${C_RED}❌ Failed to restart. Checking logs:${C_RESET}"
journalctl -u firewallfalcon-panel -n 10 --no-pager
fi
else
echo -e "\n${C_YELLOW}ℹ️ Web Panel is not installed.${C_RESET}"
fi
press_enter
;;
0) return ;;
*) invalid_option ;;
esac
done
}
uninstall_script() {
clear; show_banner
echo -e "${C_RED}=====================================================${C_RESET}"
echo -e "${C_RED} 🔥 DANGER: UNINSTALL SCRIPT & ALL DATA 🔥 ${C_RESET}"
echo -e "${C_RED}=====================================================${C_RESET}"
echo -e "${C_YELLOW}This will PERMANENTLY remove this script and all its components, including:"
echo -e " - The main command ($(command -v menu))"
echo -e " - All configuration and user data ($DB_DIR)"
echo -e " - The active limiter service ($LIMITER_SERVICE)"
echo -e " - All installed services (badvpn, udp-custom, HAProxy Edge Stack, Nginx, DNSTT)"
echo -e "\n${C_RED}This action is irreversible.${C_RESET}"
echo ""
read -p "👉 Type 'yes' to confirm and proceed with uninstallation: " confirm
if [[ "$confirm" != "yes" ]]; then
echo -e "\n${C_GREEN}✅ Uninstallation cancelled.${C_RESET}"
return
fi
local -a removable_users=()
local remove_users_confirm
local remove_users_on_uninstall=false
mapfile -t removable_users < <(get_firewallfalcon_known_users)
if [[ ${#removable_users[@]} -gt 0 ]]; then
echo -e "\n${C_YELLOW}FirewallFalcon SSH users detected on this VPS:${C_RESET} ${removable_users[*]}"
read -p "👉 Do you also want to permanently delete these SSH users before uninstalling? (y/n): " remove_users_confirm
if [[ "$remove_users_confirm" == "y" || "$remove_users_confirm" == "Y" ]]; then
remove_users_on_uninstall=true
fi
fi
export UNINSTALL_MODE="silent"
echo -e "\n${C_BLUE}--- 💥 Starting Uninstallation 💥 ---${C_RESET}"
if [[ "$remove_users_on_uninstall" == "true" ]]; then
echo -e "\n${C_BLUE}🗑️ Removing FirewallFalcon SSH users before uninstall...${C_RESET}"
delete_firewallfalcon_user_accounts "${removable_users[@]}"
fi
echo -e "\n${C_BLUE}🗑️ Removing active limiter service...${C_RESET}"
systemctl stop firewallfalcon-limiter &>/dev/null
systemctl disable firewallfalcon-limiter &>/dev/null
rm -f "$LIMITER_SERVICE"
rm -f "$LIMITER_SCRIPT"
echo -e "\n${C_BLUE}🗑️ Removing bandwidth monitoring service...${C_RESET}"
systemctl stop firewallfalcon-bandwidth &>/dev/null
systemctl disable firewallfalcon-bandwidth &>/dev/null
rm -f "$BANDWIDTH_SERVICE"
rm -f "$BANDWIDTH_SCRIPT"
rm -rf "$LEGACY_BANDWIDTH_DIR"
rm -f "$TRIAL_CLEANUP_SCRIPT"
echo -e "\n${C_BLUE}\ud83d\uddd1\ufe0f Removing SSH login banner...${C_RESET}"
rm -f "$LOGIN_INFO_SCRIPT"
rm -f "$SSHD_FF_CONFIG"
systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null
chattr -i /etc/resolv.conf &>/dev/null
purge_nginx "silent"
uninstall_dnstt
uninstall_badvpn
uninstall_udp_custom
uninstall_ssl_tunnel
uninstall_falcon_proxy
uninstall_zivpn
uninstall_web_panel
delete_dns_record
echo -e "\n${C_BLUE}🔄 Reloading systemd daemon...${C_RESET}"
systemctl daemon-reload
echo -e "\n${C_BLUE}🗑️ Removing script and configuration files...${C_RESET}"
rm -rf "$BADVPN_BUILD_DIR"
rm -rf "$UDP_CUSTOM_DIR"
rm -rf "$DB_DIR"
rm -f "$(command -v menu)"
echo -e "\n${C_GREEN}=============================================${C_RESET}"
echo -e "${C_GREEN} Script has been successfully uninstalled. ${C_RESET}"
echo -e "${C_GREEN}=============================================${C_RESET}"
echo -e "\nAll associated files and services have been removed."
echo "The 'menu' command will no longer work."
exit 0
}
# --- NEW FEATURES ---
create_trial_account() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- ⏱️ Create Trial/Test Account ---${C_RESET}"
# Ensure 'at' daemon is available
if ! command -v at &>/dev/null; then
echo -e "${C_YELLOW}⚠️ 'at' command not found. Installing...${C_RESET}"
ff_pkg_install at >/dev/null 2>&1 || {
echo -e "${C_RED}❌ Failed to install 'at'. Cannot schedule auto-expiry.${C_RESET}"
return
}
systemctl enable atd &>/dev/null
systemctl start atd &>/dev/null
fi
# Ensure atd is running
if ! systemctl is-active --quiet atd; then
systemctl start atd &>/dev/null
fi
echo -e "\n${C_CYAN}Select trial duration:${C_RESET}\n"
printf " ${C_GREEN}[ 1]${C_RESET} ⏱️ 1 Hour\n"
printf " ${C_GREEN}[ 2]${C_RESET} ⏱️ 2 Hours\n"
printf " ${C_GREEN}[ 3]${C_RESET} ⏱️ 3 Hours\n"
printf " ${C_GREEN}[ 4]${C_RESET} ⏱️ 6 Hours\n"
printf " ${C_GREEN}[ 5]${C_RESET} ⏱️ 12 Hours\n"
printf " ${C_GREEN}[ 6]${C_RESET} 📅 1 Day\n"
printf " ${C_GREEN}[ 7]${C_RESET} 📅 3 Days\n"
printf " ${C_GREEN}[ 8]${C_RESET} ⚙️ Custom (enter hours)\n"
echo -e "\n ${C_RED}[ 0]${C_RESET} ↩️ Cancel"
echo
read -p "👉 Select duration: " dur_choice
local duration_hours=0
local duration_label=""
case $dur_choice in
1) duration_hours=1; duration_label="1 Hour" ;;
2) duration_hours=2; duration_label="2 Hours" ;;
3) duration_hours=3; duration_label="3 Hours" ;;
4) duration_hours=6; duration_label="6 Hours" ;;
5) duration_hours=12; duration_label="12 Hours" ;;
6) duration_hours=24; duration_label="1 Day" ;;
7) duration_hours=72; duration_label="3 Days" ;;
8) read -p "👉 Enter custom duration in hours: " custom_hours
if ! [[ "$custom_hours" =~ ^[0-9]+$ ]] || [[ "$custom_hours" -lt 1 ]]; then
echo -e "\n${C_RED}❌ Invalid number of hours.${C_RESET}"; return
fi
duration_hours=$custom_hours
duration_label="$custom_hours Hours"
;;
0) echo -e "\n${C_YELLOW}❌ Cancelled.${C_RESET}"; return ;;
*) echo -e "\n${C_RED}❌ Invalid option.${C_RESET}"; return ;;
esac
# Username
local rand_suffix=$(tr -dc 'a-z0-9' < /dev/urandom | head -c 5)
local default_username="trial_${rand_suffix}"
read -p "👤 Username [${default_username}]: " username
username=${username:-$default_username}
if id "$username" &>/dev/null || grep -q "^$username:" "$DB_FILE"; then
echo -e "\n${C_RED}❌ Error: User '$username' already exists.${C_RESET}"; return
fi
# Password
local password=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 8)
read -p "🔑 Password [${password}]: " custom_pass
password=${custom_pass:-$password}
# Connection limit
read -p "📶 Connection limit [1]: " limit
limit=${limit:-1}
if ! [[ "$limit" =~ ^[0-9]+$ ]]; then echo -e "\n${C_RED}❌ Invalid number.${C_RESET}"; return; fi
# Bandwidth limit
read -p "📦 Bandwidth limit in GB (0 = unlimited) [0]: " bandwidth_gb
bandwidth_gb=${bandwidth_gb:-0}
if ! [[ "$bandwidth_gb" =~ ^[0-9]+\.?[0-9]*$ ]]; then echo -e "\n${C_RED}❌ Invalid number.${C_RESET}"; return; fi
# Calculate expiry
local expire_date
if [[ "$duration_hours" -ge 24 ]]; then
local days=$((duration_hours / 24))
expire_date=$(date -d "+$days days" +%Y-%m-%d)
else
# For sub-day durations, set expiry to tomorrow to be safe (at job does the real cleanup)
expire_date=$(date -d "+1 day" +%Y-%m-%d)
fi
local expiry_timestamp
expiry_timestamp=$(date -d "+${duration_hours} hours" '+%Y-%m-%d %H:%M:%S')
# Create the system user
ensure_firewallfalcon_system_group
useradd -m -s /usr/sbin/nologin "$username"
usermod -aG "$FF_USERS_GROUP" "$username" 2>/dev/null
echo "$username:$password" | chpasswd
chage -E "$expire_date" "$username"
echo "$username:$password:$expire_date:$limit:$bandwidth_gb:trial" >> "$DB_FILE"
# Schedule auto-cleanup via 'at'
echo "$TRIAL_CLEANUP_SCRIPT $username" | at now + ${duration_hours} hours 2>/dev/null
local bw_display="Unlimited"
if [[ "$bandwidth_gb" != "0" ]]; then bw_display="${bandwidth_gb} GB"; fi
clear; show_banner
echo -e "${C_GREEN}✅ Trial account created successfully!${C_RESET}\n"
echo -e "${C_YELLOW}========================================${C_RESET}"
echo -e " ⏱️ ${C_BOLD}TRIAL ACCOUNT${C_RESET}"
echo -e "${C_YELLOW}========================================${C_RESET}"
echo -e " - 👤 Username: ${C_YELLOW}$username${C_RESET}"
echo -e " - 🔑 Password: ${C_YELLOW}$password${C_RESET}"
echo -e " - ⏱️ Duration: ${C_CYAN}$duration_label${C_RESET}"
echo -e " - 🕐 Auto-expires at: ${C_RED}$expiry_timestamp${C_RESET}"
echo -e " - 📶 Connection Limit: ${C_YELLOW}$limit${C_RESET}"
echo -e " - 📦 Bandwidth Limit: ${C_YELLOW}$bw_display${C_RESET}"
echo -e "${C_YELLOW}========================================${C_RESET}"
echo -e "\n${C_DIM}The account will be automatically deleted when the trial expires.${C_RESET}"
# Auto-ask for config generation
echo
read -p "👉 Generate client config for this trial user? (y/n): " gen_conf
if [[ "$gen_conf" == "y" || "$gen_conf" == "Y" ]]; then
generate_client_config "$username" "$password"
fi
invalidate_banner_cache
refresh_dynamic_banner_routing_if_enabled
}
view_user_bandwidth() {
_select_user_interface "--- 📊 View User Bandwidth ---"
local u=$SELECTED_USER
if [[ "$u" == "NO_USERS" || -z "$u" ]]; then return; fi
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 📊 Bandwidth Details: ${C_YELLOW}$u${C_PURPLE} ---${C_RESET}\n"
local line; line=$(grep "^$u:" "$DB_FILE")
local _u _p _e _l bandwidth_gb
IFS=: read -r _u _p _e _l bandwidth_gb _ <<< "$line"
[[ -z "$bandwidth_gb" ]] && bandwidth_gb="0"
local used_bytes=0
if [[ -f "$BANDWIDTH_DIR/${u}.usage" ]]; then
read -r used_bytes < "$BANDWIDTH_DIR/${u}.usage" 2>/dev/null || used_bytes=0
[[ -z "$used_bytes" ]] && used_bytes=0
fi
local used_mb; used_mb=$(awk "BEGIN {printf \"%.2f\", $used_bytes / 1048576}")
local used_gb; used_gb=$(awk "BEGIN {printf \"%.3f\", $used_bytes / 1073741824}")
echo -e " ${C_CYAN}Data Used:${C_RESET} ${C_WHITE}${used_gb} GB${C_RESET} (${used_mb} MB)"
if [[ "$bandwidth_gb" == "0" ]]; then
echo -e " ${C_CYAN}Bandwidth Limit:${C_RESET} ${C_GREEN}Unlimited${C_RESET}"
echo -e " ${C_CYAN}Status:${C_RESET} ${C_GREEN}No quota restrictions${C_RESET}"
else
local quota_bytes; quota_bytes=$(awk "BEGIN {printf \"%.0f\", $bandwidth_gb * 1073741824}")
local percentage; percentage=$(awk "BEGIN {printf \"%.1f\", ($used_bytes / $quota_bytes) * 100}")
local remaining_bytes; remaining_bytes=$((quota_bytes - used_bytes))
if [[ "$remaining_bytes" -lt 0 ]]; then remaining_bytes=0; fi
local remaining_gb; remaining_gb=$(awk "BEGIN {printf \"%.3f\", $remaining_bytes / 1073741824}")
echo -e " ${C_CYAN}Bandwidth Limit:${C_RESET} ${C_YELLOW}${bandwidth_gb} GB${C_RESET}"
echo -e " ${C_CYAN}Remaining:${C_RESET} ${C_WHITE}${remaining_gb} GB${C_RESET}"
echo -e " ${C_CYAN}Usage:${C_RESET} ${C_WHITE}${percentage}%${C_RESET}"
# Progress bar
local bar_width=30
local filled; filled=$(awk "BEGIN {printf \"%.0f\", ($percentage / 100) * $bar_width}")
if [[ "$filled" -gt "$bar_width" ]]; then filled=$bar_width; fi
local empty=$((bar_width - filled))
local bar_color="$C_GREEN"
if (( $(awk "BEGIN {print ($percentage > 80)}" ) )); then bar_color="$C_RED"
elif (( $(awk "BEGIN {print ($percentage > 50)}" ) )); then bar_color="$C_YELLOW"
fi
printf " ${C_CYAN}Progress:${C_RESET} ${bar_color}["
for ((i=0; i/dev/null || grep -q "^$username:" "$DB_FILE"; then
echo -e "${C_RED} ⚠️ Skipping '$username' — already exists${C_RESET}"
continue
fi
local password=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 8)
useradd -m -s /usr/sbin/nologin "$username"
usermod -aG "$FF_USERS_GROUP" "$username" 2>/dev/null
echo "$username:$password" | chpasswd
chage -E "$expire_date" "$username"
echo "$username:$password:$expire_date:$limit:$bandwidth_gb:$daily_bandwidth_gb:bulk" >> "$DB_FILE"
printf " ${C_GREEN}%-20s${C_RESET} | ${C_YELLOW}%-15s${C_RESET} | ${C_CYAN}%-12s${C_RESET}\n" "$username" "$password" "$expire_date"
created=$((created + 1))
done
echo -e "${C_YELLOW}================================================================${C_RESET}"
echo -e "\n${C_GREEN}✅ Created $created users. Conn Limit: ${limit} | Total BW: ${bw_display} | Daily BW: ${daily_bw_display}${C_RESET}"
invalidate_banner_cache
refresh_dynamic_banner_routing_if_enabled
}
generate_client_config() {
local user=$1
local pass=$2
local host_ip=$(curl -s -4 icanhazip.com)
local host_domain
host_domain=$(detect_preferred_host)
[[ -z "$host_domain" ]] && host_domain="$host_ip"
echo -e "\n${C_BOLD}${C_PURPLE}--- 📱 Client Connection Configuration ---${C_RESET}"
echo -e "${C_CYAN}Copy the details below to your clipboard:${C_RESET}\n"
echo -e "${C_YELLOW}========================================${C_RESET}"
echo -e "👤 ${C_BOLD}User Details${C_RESET}"
echo -e " • Username: ${C_WHITE}$user${C_RESET}"
echo -e " • Password: ${C_WHITE}$pass${C_RESET}"
echo -e " • Host/IP : ${C_WHITE}$host_domain${C_RESET}"
echo -e "${C_YELLOW}========================================${C_RESET}"
# 1. SSH Direct
echo -e "\n🔹 ${C_BOLD}SSH Direct${C_RESET}:"
echo -e " • Host: $host_domain"
echo -e " • Port: 22"
echo -e " • payload: (Standard SSH)"
# 2. HAProxy edge stack
if systemctl is-active --quiet haproxy; then
echo -e "\n🔹 ${C_BOLD}HAProxy Edge Stack${C_RESET}:"
echo -e " • Host: $host_domain"
echo -e " • Port 80: HTTP payloads / raw SSH"
echo -e " • Port 443: TLS / SNI / SSL payloads"
echo -e " • Internal handoff: Nginx ${NGINX_INTERNAL_HTTP_PORT}/${NGINX_INTERNAL_TLS_PORT}"
echo -e " • SNI (BugHost): $host_domain (or your preferred SNI)"
elif systemctl is-active --quiet nginx; then
echo -e "\n🔹 ${C_BOLD}Internal Nginx Proxy${C_RESET}:"
echo -e " • Internal only: ${NGINX_INTERNAL_HTTP_PORT}/${NGINX_INTERNAL_TLS_PORT}"
echo -e " • Public clients should connect through HAProxy on ${EDGE_PUBLIC_HTTP_PORT}/${EDGE_PUBLIC_TLS_PORT}"
fi
# 3. UDP Custom
if systemctl is-active --quiet udp-custom; then
echo -e "\n🔹 ${C_BOLD}UDP Custom${C_RESET}:"
echo -e " • IP: $host_ip (Must use numeric IP)"
echo -e " • Port: 1-65535 (Exclude 53, 5300)"
echo -e " • Obfs: (None/Plain)"
fi
# 4. DNSTT
if systemctl is-active --quiet dnstt; then
if [ -f "$DNSTT_CONFIG_FILE" ]; then
source "$DNSTT_CONFIG_FILE"
echo -e "\n🔹 ${C_BOLD}DNSTT (SlowDNS)${C_RESET}:"
echo -e " • Nameserver: $TUNNEL_DOMAIN"
echo -e " • PubKey: $PUBLIC_KEY"
echo -e " • DNS IP: 1.1.1.1 / 8.8.8.8"
fi
fi
# 5. ZiVPN
if systemctl is-active --quiet zivpn; then
echo -e "\n🔹 ${C_BOLD}ZiVPN${C_RESET}:"
echo -e " • UDP Port: 5667"
echo -e " • Forwarded Ports: 6000-19999"
fi
echo -e "${C_YELLOW}========================================${C_RESET}"
}
client_config_menu() {
_select_user_interface "--- 📱 Generate Client Config ---"
local u=$SELECTED_USER
if [[ "$u" == "NO_USERS" || -z "$u" ]]; then return; fi
# We need to find the password. It's in the DB.
local pass=$(grep "^$u:" "$DB_FILE" | cut -d: -f2)
generate_client_config "$u" "$pass"
}
format_rate_from_kbps() {
local kbps=${1:-0}
if (( kbps >= 1024 )); then
printf "%d.%02d MB/s" $((kbps / 1024)) $((((kbps % 1024) * 100) / 1024))
else
printf "%d KB/s" "$kbps"
fi
}
# Lightweight Bash Monitor (No vnStat required)
simple_live_monitor() {
local iface=$1
local rx_file="/sys/class/net/$iface/statistics/rx_bytes"
local tx_file="/sys/class/net/$iface/statistics/tx_bytes"
local interval=2
local stop_monitor=0
local rx1 tx1 rx2 tx2 rx_diff tx_diff rx_kbs tx_kbs rx_fmt tx_fmt
if [[ -z "$iface" || ! -r "$rx_file" || ! -r "$tx_file" ]]; then
echo -e "\n${C_RED}❌ Could not read interface statistics for '${iface:-unknown}'.${C_RESET}"
return
fi
echo -e "\n${C_BLUE}⚡ Starting Lightweight Traffic Monitor for $iface...${C_RESET}"
echo -e "${C_DIM}Press [Ctrl+C] to stop.${C_RESET}\n"
read -r rx1 < "$rx_file"
read -r tx1 < "$tx_file"
printf "%-15s | %-15s\n" "⬇️ Download" "⬆️ Upload"
echo "-----------------------------------"
trap 'stop_monitor=1' INT TERM
while (( ! stop_monitor )); do
sleep "$interval"
read -r rx2 < "$rx_file" || break
read -r tx2 < "$tx_file" || break
rx_diff=$((rx2 - rx1))
tx_diff=$((tx2 - tx1))
(( rx_diff < 0 )) && rx_diff=0
(( tx_diff < 0 )) && tx_diff=0
rx_kbs=$((rx_diff / 1024 / interval))
tx_kbs=$((tx_diff / 1024 / interval))
rx_fmt=$(format_rate_from_kbps "$rx_kbs")
tx_fmt=$(format_rate_from_kbps "$tx_kbs")
printf "\r%-15s | %-15s" "$rx_fmt" "$tx_fmt"
rx1=$rx2
tx1=$tx2
done
trap - INT TERM
echo
}
traffic_monitor_menu() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 📈 Network Traffic Monitor ---${C_RESET}"
# Find active interface
local iface=$(ip -4 route ls | grep default | grep -Po '(?<=dev )(\S+)' | head -1)
echo -e "\nInterface: ${C_CYAN}${iface}${C_RESET}"
echo -e "\n${C_BOLD}Select a monitoring option:${C_RESET}\n"
printf " ${C_CHOICE}[ 1]${C_RESET} %-40s\n" "⚡ Live Monitor ${C_DIM}(Lightweight, No Install)${C_RESET}"
printf " ${C_CHOICE}[ 2]${C_RESET} %-40s\n" "📊 View Total Traffic Since Boot"
printf " ${C_CHOICE}[ 3]${C_RESET} %-40s\n" "📅 Daily/Monthly Logs ${C_DIM}(Requires vnStat)${C_RESET}"
echo -e "\n ${C_WARN}[ 0]${C_RESET} ↩️ Return"
echo
read -p "👉 Enter choice: " t_choice
case $t_choice in
1)
simple_live_monitor "$iface"
;;
2)
local rx_total=$(cat /sys/class/net/$iface/statistics/rx_bytes)
local tx_total=$(cat /sys/class/net/$iface/statistics/tx_bytes)
local rx_mb=$((rx_total / 1024 / 1024))
local tx_mb=$((tx_total / 1024 / 1024))
echo -e "\n${C_BLUE}📊 Total Traffic (Since Boot):${C_RESET}"
echo -e " ⬇️ Download: ${C_WHITE}${rx_mb} MB${C_RESET}"
echo -e " ⬆️ Upload: ${C_WHITE}${tx_mb} MB${C_RESET}"
press_enter
;;
3)
# vnStat Logic
if ! command -v vnstat &> /dev/null; then
echo -e "\n${C_YELLOW}⚠️ vnStat is not installed.${C_RESET}"
echo -e " This tool provides persistent history (Daily/Monthly reports)."
echo -e " It is lightweight but requires installation."
read -p "👉 Install vnStat now? (y/n): " confirm
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
echo -e "\n${C_BLUE}📦 Installing vnStat...${C_RESET}"
ff_pkg_install vnstat >/dev/null 2>&1 || {
echo -e "${C_RED}❌ Failed to install vnStat.${C_RESET}"
sleep 1
return
}
systemctl enable vnstat >/dev/null 2>&1
systemctl restart vnstat >/dev/null 2>&1
local default_iface=$(ip -4 route ls | grep default | grep -Po '(?<=dev )(\S+)' | head -1)
vnstat --add -i "$default_iface" >/dev/null 2>&1
echo -e "${C_GREEN}✅ Installed.${C_RESET}"
sleep 1
else
return
fi
fi
echo
vnstat -i "$iface"
echo -e "\n${C_DIM}Run 'vnstat -d' or 'vnstat -m' manually for specific views.${C_RESET}"
press_enter
;;
*) return ;;
esac
}
torrent_block_menu() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🚫 Torrent Blocking (Anti-Torrent) ---${C_RESET}"
# Check status
local torrent_status="${C_STATUS_I}Disabled${C_RESET}"
if iptables -L FORWARD | grep -q "ipp2p"; then
torrent_status="${C_STATUS_A}Enabled${C_RESET}"
elif iptables -L OUTPUT | grep -q "BitTorrent"; then
# Fallback check for string matching
torrent_status="${C_STATUS_A}Enabled${C_RESET}"
fi
echo -e "\n${C_WHITE}Current Status: ${torrent_status}${C_RESET}"
echo -e "${C_DIM}This feature uses iptables string matching to block common torrent keywords.${C_RESET}"
echo -e "\n${C_BOLD}Select an action:${C_RESET}\n"
printf " ${C_CHOICE}[ 1]${C_RESET} %-40s\n" "🔒 Enable Torrent Blocking"
printf " ${C_CHOICE}[ 2]${C_RESET} %-40s\n" "🔓 Disable Torrent Blocking"
echo -e "\n ${C_WARN}[ 0]${C_RESET} ↩️ Return"
echo
read -p "👉 Enter choice: " b_choice
case $b_choice in
1)
echo -e "\n${C_BLUE}🛡️ Applying Anti-Torrent rules...${C_RESET}"
# Clean old rules first to avoid duplicates
_flush_torrent_rules
# Block Common Torrent Ports/Keywords
# String matching using iptables extension
iptables -A FORWARD -m string --string "BitTorrent" --algo bm -j DROP
iptables -A FORWARD -m string --string "BitTorrent protocol" --algo bm -j DROP
iptables -A FORWARD -m string --string "peer_id=" --algo bm -j DROP
iptables -A FORWARD -m string --string ".torrent" --algo bm -j DROP
iptables -A FORWARD -m string --string "announce.php?passkey=" --algo bm -j DROP
iptables -A FORWARD -m string --string "torrent" --algo bm -j DROP
iptables -A FORWARD -m string --string "info_hash" --algo bm -j DROP
iptables -A FORWARD -m string --string "get_peers" --algo bm -j DROP
iptables -A FORWARD -m string --string "find_node" --algo bm -j DROP
# Same for OUTPUT to be safe
iptables -A OUTPUT -m string --string "BitTorrent" --algo bm -j DROP
iptables -A OUTPUT -m string --string "BitTorrent protocol" --algo bm -j DROP
iptables -A OUTPUT -m string --string "peer_id=" --algo bm -j DROP
iptables -A OUTPUT -m string --string ".torrent" --algo bm -j DROP
iptables -A OUTPUT -m string --string "announce.php?passkey=" --algo bm -j DROP
iptables -A OUTPUT -m string --string "torrent" --algo bm -j DROP
iptables -A OUTPUT -m string --string "info_hash" --algo bm -j DROP
iptables -A OUTPUT -m string --string "get_peers" --algo bm -j DROP
iptables -A OUTPUT -m string --string "find_node" --algo bm -j DROP
# Attempt to save if iptables-persistent exists
if ff_pkg_is_installed iptables-persistent &>/dev/null; then
netfilter-persistent save &>/dev/null
fi
echo -e "${C_GREEN}✅ Torrent Blocking Enabled.${C_RESET}"
press_enter
;;
2)
echo -e "\n${C_BLUE}🔓 Removing Anti-Torrent rules...${C_RESET}"
_flush_torrent_rules
if ff_pkg_is_installed iptables-persistent &>/dev/null; then
netfilter-persistent save &>/dev/null
fi
echo -e "${C_GREEN}✅ Torrent Blocking Disabled.${C_RESET}"
press_enter
;;
*) return ;;
esac
}
_flush_torrent_rules() {
# Helper to remove rules containing specific strings
# This is a bit brute-force but effective for this script's scope
iptables -D FORWARD -m string --string "BitTorrent" --algo bm -j DROP 2>/dev/null
iptables -D FORWARD -m string --string "BitTorrent protocol" --algo bm -j DROP 2>/dev/null
iptables -D FORWARD -m string --string "peer_id=" --algo bm -j DROP 2>/dev/null
iptables -D FORWARD -m string --string ".torrent" --algo bm -j DROP 2>/dev/null
iptables -D FORWARD -m string --string "announce.php?passkey=" --algo bm -j DROP 2>/dev/null
iptables -D FORWARD -m string --string "torrent" --algo bm -j DROP 2>/dev/null
iptables -D FORWARD -m string --string "info_hash" --algo bm -j DROP 2>/dev/null
iptables -D FORWARD -m string --string "get_peers" --algo bm -j DROP 2>/dev/null
iptables -D FORWARD -m string --string "find_node" --algo bm -j DROP 2>/dev/null
iptables -D OUTPUT -m string --string "BitTorrent" --algo bm -j DROP 2>/dev/null
iptables -D OUTPUT -m string --string "BitTorrent protocol" --algo bm -j DROP 2>/dev/null
iptables -D OUTPUT -m string --string "peer_id=" --algo bm -j DROP 2>/dev/null
iptables -D OUTPUT -m string --string ".torrent" --algo bm -j DROP 2>/dev/null
iptables -D OUTPUT -m string --string "announce.php?passkey=" --algo bm -j DROP 2>/dev/null
iptables -D OUTPUT -m string --string "torrent" --algo bm -j DROP 2>/dev/null
iptables -D OUTPUT -m string --string "info_hash" --algo bm -j DROP 2>/dev/null
iptables -D OUTPUT -m string --string "get_peers" --algo bm -j DROP 2>/dev/null
iptables -D OUTPUT -m string --string "find_node" --algo bm -j DROP 2>/dev/null
}
ssh_banner_menu() {
while true; do
show_banner
local banner_mode
local banner_status
banner_mode=$(get_ssh_banner_mode)
case "$banner_mode" in
dynamic) banner_status="${C_STATUS_A}Dynamic${C_RESET}" ;;
static) banner_status="${C_STATUS_A}Static${C_RESET}" ;;
*) banner_status="${C_STATUS_I}Disabled${C_RESET}" ;;
esac
echo -e "\n ${C_TITLE}═════════════════[ ${C_BOLD}🎨 SSH BANNER MODE: ${banner_status} ${C_RESET}${C_TITLE}]═════════════════${C_RESET}"
echo -e "${C_DIM}Static mode uses 'Banner $SSH_BANNER_FILE'. Dynamic mode shows per-user account info.${C_RESET}"
printf " ${C_CHOICE}[ 1]${C_RESET} %-40s\n" "✨ Enable Dynamic Account Banner"
printf " ${C_CHOICE}[ 2]${C_RESET} %-40s\n" "📋 Paste or Replace Static Banner"
printf " ${C_CHOICE}[ 3]${C_RESET} %-40s\n" "👁️ View Current Static Banner"
printf " ${C_CHOICE}[ 4]${C_RESET} %-40s\n" "📝 Preview Dynamic Banner"
printf " ${C_DANGER}[ 5]${C_RESET} %-40s\n" "🗑️ Disable All SSH Banners"
echo -e " ${C_DIM}~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~${C_RESET}"
echo -e " ${C_WARN}[ 0]${C_RESET} ↩️ Return"
echo
if ! read -r -p "$(echo -e ${C_PROMPT}"👉 Select an option: "${C_RESET})" choice; then
echo
return
fi
case $choice in
1)
if setup_ssh_login_info; then
echo -e "\n${C_GREEN}✅ Dynamic account banner enabled.${C_RESET}"
echo -e "${C_DIM}Users will now see their account info banner instead of the static banner.${C_RESET}"
fi
press_enter
;;
2) set_ssh_banner_paste ;;
3) view_ssh_banner ;;
4) preview_dynamic_ssh_banner ;;
5) remove_ssh_banner ;;
0) return ;;
*) echo -e "\n${C_RED}❌ Invalid option.${C_RESET}" && sleep 1 ;;
esac
done
}
auto_reboot_menu() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 🔄 Auto-Reboot Management ---${C_RESET}"
# Check status
local cron_check=$(crontab -l 2>/dev/null | grep "systemctl reboot")
local status="${C_STATUS_I}Disabled${C_RESET}"
if [[ -n "$cron_check" ]]; then
status="${C_STATUS_A}Active (Midnight)${C_RESET}"
fi
echo -e "\n${C_WHITE}Current Status: ${status}${C_RESET}"
echo -e "\n${C_BOLD}Select an action:${C_RESET}\n"
printf " ${C_CHOICE}[ 1]${C_RESET} %-40s\n" "🕐 Enable Daily Reboot (00:00 midnight)"
printf " ${C_CHOICE}[ 2]${C_RESET} %-40s\n" "❌ Disable Auto-Reboot"
echo -e "\n ${C_WARN}[ 0]${C_RESET} ↩️ Return"
echo
read -p "👉 Enter choice: " r_choice
case $r_choice in
1)
# Remove existing to prevent duplicates
(crontab -l 2>/dev/null | grep -v "systemctl reboot") | crontab -
# Add new job
(crontab -l 2>/dev/null; echo "0 0 * * * systemctl reboot") | crontab -
echo -e "\n${C_GREEN}✅ Auto-reboot scheduled for every day at 00:00.${C_RESET}"
press_enter
;;
2)
(crontab -l 2>/dev/null | grep -v "systemctl reboot") | crontab -
echo -e "\n${C_GREEN}✅ Auto-reboot disabled.${C_RESET}"
press_enter
;;
*) return ;;
esac
}
press_enter() {
echo -e "\nPress ${C_YELLOW}[Enter]${C_RESET} to return to the menu..." && read -r || true
}
invalid_option() {
echo -e "\n${C_RED}❌ Invalid option.${C_RESET}" && sleep 1
}
main_menu() {
while true; do
export UNINSTALL_MODE="interactive"
show_banner
echo
echo -e " ${C_TITLE}═══════════════════[ ${C_BOLD}👤 USER MANAGEMENT ${C_RESET}${C_TITLE}]═══════════════════${C_RESET}"
printf " ${C_CHOICE}[%2s]${C_RESET} %-28s ${C_CHOICE}[%2s]${C_RESET} %-28s\n" "1" "✨ Create New User" "2" "🗑️ Delete User"
printf " ${C_CHOICE}[%2s]${C_RESET} %-28s ${C_CHOICE}[%2s]${C_RESET} %-28s\n" "3" "🔄 Renew User Account" "4" "🔒 Lock User Account"
printf " ${C_CHOICE}[%2s]${C_RESET} %-28s ${C_CHOICE}[%2s]${C_RESET} %-28s\n" "5" "🔓 Unlock User Account" "6" "✏️ Edit User Details"
printf " ${C_CHOICE}[%2s]${C_RESET} %-28s ${C_CHOICE}[%2s]${C_RESET} %-28s\n" "7" "📋 List Managed Users" "8" "📱 Generate Client Config"
printf " ${C_CHOICE}[%2s]${C_RESET} %-28s ${C_CHOICE}[%2s]${C_RESET} %-28s\n" "9" "⏱️ Create Trial Account" "10" "📊 View User Bandwidth"
printf " ${C_CHOICE}[%2s]${C_RESET} %-28s\n" "11" "👥 Bulk Create Users"
echo
echo -e " ${C_TITLE}══════════════[ ${C_BOLD}🌐 VPN & PROTOCOLS ${C_RESET}${C_TITLE}]═══════════════${C_RESET}"
printf " ${C_CHOICE}[%2s]${C_RESET} %-28s ${C_CHOICE}[%2s]${C_RESET} %-28s\n" "12" "🔌 Protocol Manager" "13" "📈 Traffic Monitor (Lite)"
printf " ${C_CHOICE}[%2s]${C_RESET} %-28s\n" "14" "🚫 Block Torrent (Anti-P2P)"
echo
echo -e " ${C_TITLE}══════════════[ ${C_BOLD}⚙️ SYSTEM SETTINGS ${C_RESET}${C_TITLE}]═══════════════${C_RESET}"
printf " ${C_CHOICE}[%2s]${C_RESET} %-28s ${C_CHOICE}[%2s]${C_RESET} %-28s\n" "15" "🌐 Free Domain (deSEC)" "16" "🎨 SSH Banner Config"
printf " ${C_CHOICE}[%2s]${C_RESET} %-28s ${C_CHOICE}[%2s]${C_RESET} %-28s\n" "17" "🔄 Auto-Reboot Task" "18" "💾 Backup User Data"
printf " ${C_CHOICE}[%2s]${C_RESET} %-28s ${C_CHOICE}[%2s]${C_RESET} %-28s\n" "19" "📥 Restore User Data" "20" "🧹 Cleanup Expired Users"
printf " ${C_CHOICE}[%2s]${C_RESET} %-28s\n" "21" "🌐 Web Control Panel"
echo
echo -e " ${C_DANGER}═══════════════════[ ${C_BOLD}🔥 DANGER ZONE ${C_RESET}${C_DANGER}]═══════════════════${C_RESET}"
echo -e " ${C_DANGER}[99]${C_RESET} Uninstall Script ${C_WARN}[ 0]${C_RESET} Exit"
echo
if ! read -r -p "$(echo -e ${C_PROMPT}"👉 Select an option: "${C_RESET})" choice; then
echo
exit 0
fi
case $choice in
1) create_user; press_enter ;;
2) delete_user; press_enter ;;
3) renew_user; press_enter ;;
4) lock_user; press_enter ;;
5) unlock_user; press_enter ;;
6) edit_user; press_enter ;;
7) list_users; press_enter ;;
8) client_config_menu; press_enter ;;
9) create_trial_account; press_enter ;;
10) view_user_bandwidth; press_enter ;;
11) bulk_create_users; press_enter ;;
12) protocol_menu ;;
13) traffic_monitor_menu ;;
14) torrent_block_menu ;;
15) dns_menu; press_enter ;;
16) ssh_banner_menu ;;
17) auto_reboot_menu ;;
18) backup_user_data; press_enter ;;
19) restore_user_data; press_enter ;;
20) cleanup_expired; press_enter ;;
21) web_panel_menu ;;
99) uninstall_script ;;
0) exit 0 ;;
*) invalid_option ;;
esac
done
}
if [[ "$1" == "--install-setup" ]]; then
initial_setup
exit 0
fi
require_interactive_terminal
sync_runtime_components_if_needed
main_menu