- 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>
5423 lines
220 KiB
Bash
5423 lines
220 KiB
Bash
#!/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
|