Files
SSH-Manager/menu.sh
T
Yasin DemirandClaude Opus 5 b0a0aa9e3c Fix trial cleanup, users.db perms, DB field escaping and /tmp races
- create_trial_account wrote a 6-field record, so the marker landed in
  field 6 while firewallfalcon-trial-cleanup.sh reads field 7. Trials were
  never actually removed. Write the daily-bandwidth field so the layout
  matches, and drop a <user>.trial_expiry stamp so the limiter's sweep --
  previously dead code, nothing ever created those files -- can act as a
  fallback when atd is unavailable.
- create_user tagged every normal account as "trial"; use "normal" so the
  now-working cleanup cannot delete a regular user. Also remove the
  trial_expiry stamp when an account is deleted.
- ensure_firewallfalcon_dirs now chmods users.db to 0600; it stores
  cleartext passwords and was created world-readable by touch.
- Replace the `sed -i "s/^user:.*/..."` record updaters with an awk-based
  db_set_user_field. Values containing / or & are now stored literally and
  the trailing marker field is preserved (renew_user also truncated the
  record to five fields).
- Validate operator-supplied passwords: ':' breaks the record layout, a
  backslash is eaten by awk -v, quotes and whitespace break the consumers.
- Use mktemp instead of the fixed /tmp/ff_banners_new.conf and
  /tmp/badvpn_build paths, which root wrote in a world-writable directory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 06:18:25 +03:00

5423 lines
220 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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"
# users.db holds cleartext credentials and lives on a box where the managed
# users have accounts; keep it readable by root only.
chmod 600 "$DB_FILE" 2>/dev/null
}
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"
}
# users.db fields: 1=user 2=pass 3=expiry 4=conn_limit 5=bandwidth 6=daily_bandwidth 7=marker
DB_FIELD_PASS=2
DB_FIELD_EXPIRY=3
DB_FIELD_LIMIT=4
DB_FIELD_BW=5
DB_FIELD_DAILY_BW=6
# Rewrite a single field of a user's record. Uses awk instead of `sed s/^user:.*/.../`
# so that values containing / & \ are stored literally, and so the trailing marker
# field is preserved instead of being dropped.
db_set_user_field() {
local username="$1" field="$2" value="$3" tmp
[[ -f "$DB_FILE" ]] || return 1
tmp=$(mktemp) || return 1
if ! awk -F: -v OFS=: -v u="$username" -v f="$field" -v v="$value" \
'$1 == u { $f = v } { print }' "$DB_FILE" > "$tmp"; then
rm -f "$tmp"
return 1
fi
# Copy contents rather than mv so the 0600 mode of users.db survives.
cat "$tmp" > "$DB_FILE"
rm -f "$tmp"
}
# Reject passwords that would corrupt the ':'-delimited record or the shell
# pipelines that consume it.
ff_is_valid_password() {
local pw="$1"
if [[ -z "$pw" ]]; then
echo -e "\n${C_RED}❌ Password cannot be empty.${C_RESET}"
return 1
fi
# Quoted literals rather than backslash-escaped case patterns: the latter are
# easy to get subtly wrong. ':' breaks the record layout, a backslash is eaten
# by awk -v, and quotes/whitespace break the shell pipelines that consume it.
if [[ "$pw" == *:* || "$pw" == *'\'* || "$pw" == *"'"* || "$pw" == *'"'* ]] \
|| [[ "$pw" =~ [[:space:]] ]]; then
echo -e "\n${C_RED}❌ Password cannot contain ':', backslash, quotes or whitespace.${C_RESET}"
return 1
fi
return 0
}
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 -f "$BANDWIDTH_DIR/${username}.trial_expiry"
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="<br><font color=\"yellow\"><b> ✨ ACCOUNT STATUS ✨ </b></font><br><br>"
banner_content+="<font color=\"white\">👤 <b>Username :</b> $user</font><br>"
banner_content+="<font color=\"white\">📅 <b>Expiration :</b> $expiry ($days_left)</font><br>"
if [[ "$bandwidth_gb" != "0" ]]; then
banner_content+="<font color=\"white\">📊 <b>Total BW :</b> $bw_info</font><br>"
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+="<font color=\"white\">📊 <b>Daily BW :</b> $daily_bw_info</font><br>"
fi
banner_content+="<font color=\"white\">🔌 <b>Sessions :</b> $online_count/$limit</font><br><br>"
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 <username>
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
# mktemp, not a fixed /tmp name: root writes this file and /tmp is world-writable.
tmp_conf=$(mktemp) || return
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"
chmod 644 "$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
elif ff_is_valid_password "$password"; then
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:normal" >> "$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}"
elif ! ff_is_valid_password "$new_pass"; then
continue
fi
echo "$username:$new_pass" | chpasswd
db_set_user_field "$username" "$DB_FIELD_PASS" "$new_pass"
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"
db_set_user_field "$username" "$DB_FIELD_EXPIRY" "$new_expire_date"
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
db_set_user_field "$username" "$DB_FIELD_LIMIT" "$new_limit"
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
db_set_user_field "$username" "$DB_FIELD_BW" "$new_bw"
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
db_set_user_field "$username" "$DB_FIELD_DAILY_BW" "$new_daily_bw"
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"
db_set_user_field "$u" "$DB_FIELD_EXPIRY" "$new_expire_date"
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
# mktemp -d, not a fixed /tmp path: this is built and copied from as root.
local temp_build
temp_build=$(mktemp -d) || return
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" <<EOF
{
"listen": ":36712",
"stream_buffer": 33554432,
"receive_buffer": 83886080,
"auth": {
"mode": "passwords"
}
}
EOF
chmod 644 "$UDP_CUSTOM_DIR/config.json"
echo -e "\n${C_GREEN}📝 Creating udpgw systemd service file...${C_RESET}"
cat > "$UDPGW_SERVICE_FILE" <<EOF
[Unit]
Description=FirewallFalcon UDPGW Backend
After=network.target
[Service]
User=root
Type=simple
ExecStart=$UDPGW_BINARY --listen-addr 127.0.0.1:7800 --max-clients 1000 --max-connections-for-client 100
Restart=always
RestartSec=2s
[Install]
WantedBy=multi-user.target
EOF
echo -e "\n${C_GREEN}📝 Creating systemd service file...${C_RESET}"
cat > "$UDP_CUSTOM_SERVICE_FILE" <<EOF
[Unit]
Description=UDP Custom by FirewallFalcon
After=network.target
[Service]
User=root
Type=simple
ExecStart=$UDP_CUSTOM_DIR/udp-custom server
WorkingDirectory=$UDP_CUSTOM_DIR/
Restart=always
RestartSec=2s
[Install]
WantedBy=multi-user.target
EOF
echo -e "\n${C_GREEN}▶️ Enabling and starting udp-custom service...${C_RESET}"
systemctl daemon-reload
systemctl enable udpgw.service
systemctl start udpgw.service
systemctl enable udp-custom.service
systemctl start udp-custom.service
sleep 2
if systemctl is-active --quiet udpgw && systemctl is-active --quiet udp-custom; then
echo -e "\n${C_GREEN}✅ SUCCESS: udp-custom is installed and active.${C_RESET}"
else
echo -e "\n${C_RED}❌ ERROR: udp-custom service failed to start.${C_RESET}"
echo -e "${C_YELLOW}️ Displaying last 15 lines of the udp-custom and udpgw logs for diagnostics:${C_RESET}"
journalctl -u udp-custom.service -n 15 --no-pager
journalctl -u udpgw.service -n 15 --no-pager
fi
}
uninstall_udp_custom() {
echo -e "\n${C_BOLD}${C_PURPLE}--- 🗑️ Uninstalling udp-custom ---${C_RESET}"
if [ ! -f "$UDP_CUSTOM_SERVICE_FILE" ] && [ ! -f "$UDPGW_SERVICE_FILE" ]; then
echo -e "${C_YELLOW}️ udp-custom is not installed, skipping.${C_RESET}"
return
fi
echo -e "${C_GREEN}🛑 Stopping and disabling udpgw service...${C_RESET}"
systemctl stop udpgw.service >/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" <<EOF
EDGE_CERT_MODE="$cert_mode"
EDGE_DOMAIN="$cert_domain"
EDGE_EMAIL="$cert_email"
EOF
}
detect_preferred_host() {
local host_domain=""
load_edge_cert_info
if [[ -n "$EDGE_DOMAIN" ]]; then
host_domain="$EDGE_DOMAIN"
fi
if [[ -z "$host_domain" && -f "$DNS_INFO_FILE" ]]; then
host_domain=$(grep 'FULL_DOMAIN' "$DNS_INFO_FILE" | cut -d'"' -f2)
fi
if [[ -z "$host_domain" && -f "$NGINX_CONFIG_FILE" ]]; then
local nginx_domain
nginx_domain=$(grep -oP 'server_name \K[^\s;]+' "$NGINX_CONFIG_FILE" 2>/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" <<EOF
server {
listen 127.0.0.1:${NGINX_INTERNAL_HTTP_PORT} default_server;
listen 127.0.0.1:${NGINX_INTERNAL_TLS_PORT} ssl http2 default_server;
server_tokens off;
server_name ${server_name};
ssl_certificate ${SSL_CERT_CHAIN_FILE};
ssl_certificate_key ${SSL_CERT_KEY_FILE};
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!eNULL:!MD5:!DES:!RC4:!ADH:!SSLv3:!EXP:!PSK:!DSS;
resolver 1.1.1.1 8.8.8.8 ipv6=off valid=300s;
location ~ ^/(?<fwdport>\d+)/(?<fwdpath>.*)$ {
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" <<EOF
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
defaults
log global
mode tcp
option tcplog
option dontlognull
timeout connect 5s
timeout client 24h
timeout server 24h
# ====================================================================
# TIER 1: PORT ${EDGE_PUBLIC_HTTP_PORT} (Cleartext Payloads & Raw SSH)
# ====================================================================
frontend port_80_edge
bind *:${EDGE_PUBLIC_HTTP_PORT}
mode tcp
tcp-request inspect-delay 2s
acl is_ssh payload(0,7) -m bin 5353482d322e30
tcp-request content accept if is_ssh
tcp-request content accept if HTTP
use_backend direct_ssh if is_ssh
default_backend nginx_cleartext
# ====================================================================
# TIER 1: PORT ${EDGE_PUBLIC_TLS_PORT} (TLS v2ray, SSL Payloads, Raw SSH)
# ====================================================================
frontend port_443_edge
bind *:${EDGE_PUBLIC_TLS_PORT}
mode tcp
tcp-request inspect-delay 2s
acl is_ssh payload(0,7) -m bin 5353482d322e30
acl is_tls req.ssl_hello_type 1
acl has_web_alpn req.ssl_alpn -m sub h2 http/1.1
tcp-request content accept if is_ssh
tcp-request content accept if HTTP
tcp-request content accept if is_tls
use_backend direct_ssh if is_ssh
use_backend nginx_cleartext if HTTP
use_backend nginx_tls if is_tls has_web_alpn
default_backend loopback_ssl_terminator
# ====================================================================
# TIER 2: INTERNAL DECRYPTOR (Only for Any-SNI SSH-TLS)
# ====================================================================
frontend internal_decryptor
bind 127.0.0.1:${HAPROXY_INTERNAL_DECRYPT_PORT} ssl crt ${SSL_CERT_FILE}
mode tcp
tcp-request inspect-delay 2s
acl is_ssh payload(0,7) -m bin 5353482d322e30
tcp-request content accept if is_ssh
tcp-request content accept if HTTP
use_backend direct_ssh if is_ssh
default_backend nginx_cleartext
# ====================================================================
# DESTINATION BACKENDS (Clean handoffs, no proxy headers)
# ====================================================================
backend direct_ssh
mode tcp
server ssh_server 127.0.0.1:22
backend nginx_cleartext
mode tcp
server nginx_8880 127.0.0.1:${NGINX_INTERNAL_HTTP_PORT}
backend nginx_tls
mode tcp
server nginx_8443 127.0.0.1:${NGINX_INTERNAL_TLS_PORT}
backend loopback_ssl_terminator
mode tcp
server haproxy_ssl 127.0.0.1:${HAPROXY_INTERNAL_DECRYPT_PORT}
EOF
}
save_edge_ports_info() {
cat > "$NGINX_PORTS_FILE" <<EOF
EDGE_HTTP_PORT="${EDGE_PUBLIC_HTTP_PORT}"
EDGE_TLS_PORT="${EDGE_PUBLIC_TLS_PORT}"
HTTP_PORTS="${NGINX_INTERNAL_HTTP_PORT}"
TLS_PORTS="${NGINX_INTERNAL_TLS_PORT}"
EOF
}
configure_edge_stack() {
local server_name="$1"
[[ -z "$server_name" ]] && server_name="_"
backup_edge_configs
echo -e "\n${C_BLUE}📝 Writing internal Nginx config (127.0.0.1:${NGINX_INTERNAL_HTTP_PORT}/${NGINX_INTERNAL_TLS_PORT})...${C_RESET}"
write_internal_nginx_config "$server_name"
echo -e "${C_BLUE}📝 Writing HAProxy edge config (${EDGE_PUBLIC_HTTP_PORT}/${EDGE_PUBLIC_TLS_PORT})...${C_RESET}"
write_haproxy_edge_config
echo -e "\n${C_BLUE}🧪 Validating Nginx configuration...${C_RESET}"
if ! nginx -t >/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" <<EOF
global
log /dev/log local0
log /dev/log local1 notice
defaults
log global
EOF
fi
local delete_cert="n"
if [[ "$UNINSTALL_MODE" == "silent" ]]; then
delete_cert="y"
elif [ -f "$SSL_CERT_FILE" ] || [ -f "$SSL_CERT_CHAIN_FILE" ] || [ -f "$SSL_CERT_KEY_FILE" ]; then
if systemctl is-active --quiet nginx; then
echo -e "${C_YELLOW}⚠️ The shared certificate is also used by the internal Nginx proxy.${C_RESET}"
fi
read -p "👉 Delete the shared TLS certificate too? (y/n): " delete_cert
fi
if [[ "$delete_cert" == "y" || "$delete_cert" == "Y" ]]; then
if systemctl is-active --quiet nginx; then
echo -e "${C_GREEN}🛑 Stopping Nginx because the shared certificate is being removed...${C_RESET}"
systemctl stop nginx >/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" <<EOF
[Unit]
Description=Falcon Proxy ($SELECTED_VERSION)
After=network.target
[Service]
User=root
Type=simple
ExecStart=$FALCONPROXY_BINARY -p $ports
Restart=always
RestartSec=2s
[Install]
WantedBy=default.target
EOF
echo -e "\n${C_GREEN}💾 Saving configuration...${C_RESET}"
cat > "$FALCONPROXY_CONFIG_FILE" <<EOF
PORTS="$ports"
INSTALLED_VERSION="$SELECTED_VERSION"
EOF
echo -e "\n${C_GREEN}▶️ Enabling and starting Falcon Proxy service...${C_RESET}"
systemctl daemon-reload
systemctl enable falconproxy.service
systemctl restart falconproxy.service
sleep 2
if systemctl is-active --quiet falconproxy; then
echo -e "\n${C_GREEN}✅ SUCCESS: Falcon Proxy $SELECTED_VERSION is installed and active.${C_RESET}"
echo -e " Listening on port(s): ${C_YELLOW}$ports${C_RESET}"
else
echo -e "\n${C_RED}❌ ERROR: Falcon Proxy service failed to start.${C_RESET}"
echo -e "${C_YELLOW}️ Displaying last 15 lines of the service log for diagnostics:${C_RESET}"
journalctl -u falconproxy.service -n 15 --no-pager
fi
}
uninstall_falcon_proxy() {
echo -e "\n${C_BOLD}${C_PURPLE}--- 🗑️ Uninstalling Falcon Proxy ---${C_RESET}"
if [ ! -f "$FALCONPROXY_SERVICE_FILE" ]; then
echo -e "${C_YELLOW}️ Falcon Proxy is not installed, skipping.${C_RESET}"
return
fi
echo -e "${C_GREEN}🛑 Stopping and disabling Falcon Proxy service...${C_RESET}"
systemctl stop falconproxy.service >/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 <<EOF > "$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 <<EOF > "$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
if [[ -n "$custom_pass" ]]; then
ff_is_valid_password "$custom_pass" || return
password="$custom_pass"
fi
# 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"
# Keep the 7-field layout: the cleanup script reads the marker from field 7,
# so the daily-bandwidth field must be present even though trials do not set one.
echo "$username:$password:$expire_date:$limit:$bandwidth_gb:0:trial" >> "$DB_FILE"
# Schedule auto-cleanup via 'at'
echo "$TRIAL_CLEANUP_SCRIPT $username" | at now + ${duration_hours} hours 2>/dev/null
# Fallback for the limiter's trial sweep, in case atd is stopped or the job is lost.
mkdir -p "$BANDWIDTH_DIR"
date -d "+${duration_hours} hours" +%s > "$BANDWIDTH_DIR/${username}.trial_expiry"
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<filled; i++)); do printf "█"; done
for ((i=0; i<empty; i++)); do printf "░"; done
printf "]${C_RESET} ${percentage}%%\n"
if [[ "$used_bytes" -ge "$quota_bytes" ]]; then
echo -e "\n ${C_RED}⚠️ USER HAS EXCEEDED BANDWIDTH QUOTA — ACCOUNT LOCKED${C_RESET}"
fi
fi
}
bulk_create_users() {
clear; show_banner
echo -e "${C_BOLD}${C_PURPLE}--- 👥 Bulk Create Users ---${C_RESET}"
read -p "👉 Enter username prefix (e.g., 'user'): " prefix
if [[ -z "$prefix" ]]; then echo -e "\n${C_RED}❌ Prefix cannot be empty.${C_RESET}"; return; fi
read -p "🔢 How many users to create? " count
if ! [[ "$count" =~ ^[0-9]+$ ]] || [[ "$count" -lt 1 ]] || [[ "$count" -gt 100 ]]; then
echo -e "\n${C_RED}❌ Invalid count (1-100).${C_RESET}"; return
fi
read -p "🗓️ 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 "📶 Connection limit per user [1]: " limit
limit=${limit:-1}
if ! [[ "$limit" =~ ^[0-9]+$ ]]; then echo -e "\n${C_RED}❌ Invalid number.${C_RESET}"; return; fi
read -p "📦 Bandwidth limit in GB per user (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 "📦 DAILY bandwidth limit in GB per user (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)
local bw_display="Unlimited"; [[ "$bandwidth_gb" != "0" ]] && bw_display="${bandwidth_gb} GB"
local daily_bw_display="Unlimited"; [[ "$daily_bandwidth_gb" != "0" ]] && daily_bw_display="${daily_bandwidth_gb} GB/day"
ensure_firewallfalcon_system_group
echo -e "\n${C_BLUE}⚙️ Creating $count users with prefix '${prefix}'...${C_RESET}\n"
echo -e "${C_YELLOW}================================================================${C_RESET}"
printf "${C_BOLD}${C_WHITE}%-20s | %-15s | %-12s${C_RESET}\n" "USERNAME" "PASSWORD" "EXPIRES"
echo -e "${C_YELLOW}----------------------------------------------------------------${C_RESET}"
local created=0
for ((i=1; i<=count; i++)); do
local username="${prefix}${i}"
if id "$username" &>/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