mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-02-28 13:13:00 +00:00
Merge branch 'main' into main
This commit is contained in:
commit
8ae95f65ce
11 changed files with 318 additions and 102 deletions
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -18,6 +18,7 @@ on:
|
||||||
- 'go.mod'
|
- 'go.mod'
|
||||||
- 'go.sum'
|
- 'go.sum'
|
||||||
- 'x-ui.service.debian'
|
- 'x-ui.service.debian'
|
||||||
|
- 'x-ui.service.arch'
|
||||||
- 'x-ui.service.rhel'
|
- 'x-ui.service.rhel'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
@ -80,6 +81,7 @@ jobs:
|
||||||
mkdir x-ui
|
mkdir x-ui
|
||||||
cp xui-release x-ui/
|
cp xui-release x-ui/
|
||||||
cp x-ui.service.debian x-ui/
|
cp x-ui.service.debian x-ui/
|
||||||
|
cp x-ui.service.arch x-ui/
|
||||||
cp x-ui.service.rhel x-ui/
|
cp x-ui.service.rhel x-ui/
|
||||||
cp x-ui.sh x-ui/
|
cp x-ui.sh x-ui/
|
||||||
mv x-ui/xui-release x-ui/x-ui
|
mv x-ui/xui-release x-ui/x-ui
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ RUN apk add --no-cache --update \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
tzdata \
|
tzdata \
|
||||||
fail2ban \
|
fail2ban \
|
||||||
bash
|
bash \
|
||||||
|
curl
|
||||||
|
|
||||||
COPY --from=builder /app/build/ /app/
|
COPY --from=builder /app/build/ /app/
|
||||||
COPY --from=builder /app/DockerEntrypoint.sh /app/
|
COPY --from=builder /app/DockerEntrypoint.sh /app/
|
||||||
|
|
|
||||||
|
|
@ -80,9 +80,12 @@ type HistoryOfSeeders struct {
|
||||||
// GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
|
// GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
|
||||||
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
||||||
listen := i.Listen
|
listen := i.Listen
|
||||||
if listen != "" {
|
// Default to 0.0.0.0 (all interfaces) when listen is empty
|
||||||
listen = fmt.Sprintf("\"%v\"", listen)
|
// This ensures proper dual-stack IPv4/IPv6 binding in systems where bindv6only=0
|
||||||
|
if listen == "" {
|
||||||
|
listen = "0.0.0.0"
|
||||||
}
|
}
|
||||||
|
listen = fmt.Sprintf("\"%v\"", listen)
|
||||||
return &xray.InboundConfig{
|
return &xray.InboundConfig{
|
||||||
Listen: json_util.RawMessage(listen),
|
Listen: json_util.RawMessage(listen),
|
||||||
Port: i.Port,
|
Port: i.Port,
|
||||||
|
|
|
||||||
90
install.sh
90
install.sh
|
|
@ -53,35 +53,52 @@ is_ip() {
|
||||||
is_ipv4 "$1" || is_ipv6 "$1"
|
is_ipv4 "$1" || is_ipv6 "$1"
|
||||||
}
|
}
|
||||||
is_domain() {
|
is_domain() {
|
||||||
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1
|
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Port helpers
|
||||||
|
is_port_in_use() {
|
||||||
|
local port="$1"
|
||||||
|
if command -v ss >/dev/null 2>&1; then
|
||||||
|
ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if command -v netstat >/dev/null 2>&1; then
|
||||||
|
netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if command -v lsof >/dev/null 2>&1; then
|
||||||
|
lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
install_base() {
|
install_base() {
|
||||||
case "${release}" in
|
case "${release}" in
|
||||||
ubuntu | debian | armbian)
|
ubuntu | debian | armbian)
|
||||||
apt-get update && apt-get install -y -q curl tar tzdata socat
|
apt-get update && apt-get install -y -q curl tar tzdata socat ca-certificates
|
||||||
;;
|
;;
|
||||||
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
|
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
|
||||||
dnf -y update && dnf install -y -q curl tar tzdata socat
|
dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates
|
||||||
;;
|
;;
|
||||||
centos)
|
centos)
|
||||||
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
||||||
yum -y update && yum install -y curl tar tzdata socat
|
yum -y update && yum install -y curl tar tzdata socat ca-certificates
|
||||||
else
|
else
|
||||||
dnf -y update && dnf install -y -q curl tar tzdata socat
|
dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
arch | manjaro | parch)
|
arch | manjaro | parch)
|
||||||
pacman -Syu && pacman -Syu --noconfirm curl tar tzdata socat
|
pacman -Syu && pacman -Syu --noconfirm curl tar tzdata socat ca-certificates
|
||||||
;;
|
;;
|
||||||
opensuse-tumbleweed | opensuse-leap)
|
opensuse-tumbleweed | opensuse-leap)
|
||||||
zypper refresh && zypper -q install -y curl tar timezone socat
|
zypper refresh && zypper -q install -y curl tar timezone socat ca-certificates
|
||||||
;;
|
;;
|
||||||
alpine)
|
alpine)
|
||||||
apk update && apk add curl tar tzdata socat
|
apk update && apk add curl tar tzdata socat ca-certificates
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
apt-get update && apt-get install -y -q curl tar tzdata socat
|
apt-get update && apt-get install -y -q curl tar tzdata socat ca-certificates
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
@ -180,7 +197,7 @@ setup_ip_certificate() {
|
||||||
|
|
||||||
echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
|
echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
|
||||||
echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}"
|
echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}"
|
||||||
echo -e "${yellow}Port 80 must be open and accessible from the internet.${plain}"
|
echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}"
|
||||||
|
|
||||||
# Check for acme.sh
|
# Check for acme.sh
|
||||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||||
|
|
@ -216,6 +233,43 @@ setup_ip_certificate() {
|
||||||
# Set reload command for auto-renewal (add || true so it doesn't fail during first install)
|
# Set reload command for auto-renewal (add || true so it doesn't fail during first install)
|
||||||
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true"
|
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true"
|
||||||
|
|
||||||
|
# Choose port for HTTP-01 listener (default 80, prompt override)
|
||||||
|
local WebPort=""
|
||||||
|
read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort
|
||||||
|
WebPort="${WebPort:-80}"
|
||||||
|
if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then
|
||||||
|
echo -e "${red}Invalid port provided. Falling back to 80.${plain}"
|
||||||
|
WebPort=80
|
||||||
|
fi
|
||||||
|
echo -e "${green}Using port ${WebPort} for standalone validation.${plain}"
|
||||||
|
if [[ "${WebPort}" -ne 80 ]]; then
|
||||||
|
echo -e "${yellow}Reminder: Let's Encrypt still connects on port 80; forward external port 80 to ${WebPort}.${plain}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure chosen port is available
|
||||||
|
while true; do
|
||||||
|
if is_port_in_use "${WebPort}"; then
|
||||||
|
echo -e "${yellow}Port ${WebPort} is in use.${plain}"
|
||||||
|
|
||||||
|
local alt_port=""
|
||||||
|
read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port
|
||||||
|
alt_port="${alt_port// /}"
|
||||||
|
if [[ -z "${alt_port}" ]]; then
|
||||||
|
echo -e "${red}Port ${WebPort} is busy; cannot proceed.${plain}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then
|
||||||
|
echo -e "${red}Invalid port provided.${plain}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
WebPort="${alt_port}"
|
||||||
|
continue
|
||||||
|
else
|
||||||
|
echo -e "${green}Port ${WebPort} is free and ready for standalone validation.${plain}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
# Issue certificate with shortlived profile
|
# Issue certificate with shortlived profile
|
||||||
echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}"
|
echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}"
|
||||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1
|
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1
|
||||||
|
|
@ -226,12 +280,12 @@ setup_ip_certificate() {
|
||||||
--server letsencrypt \
|
--server letsencrypt \
|
||||||
--certificate-profile shortlived \
|
--certificate-profile shortlived \
|
||||||
--days 6 \
|
--days 6 \
|
||||||
--httpport 80 \
|
--httpport ${WebPort} \
|
||||||
--force
|
--force
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo -e "${red}Failed to issue IP certificate${plain}"
|
echo -e "${red}Failed to issue IP certificate${plain}"
|
||||||
echo -e "${yellow}Please ensure port 80 is open and accessible from the internet${plain}"
|
echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}"
|
||||||
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
|
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
|
||||||
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
|
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
|
||||||
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
|
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
|
||||||
|
|
@ -764,6 +818,15 @@ install_x-ui() {
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
arch | manjaro | parch)
|
||||||
|
if [ -f "x-ui.service.arch" ]; then
|
||||||
|
echo -e "${green}Found x-ui.service.arch in extracted files, installing...${plain}"
|
||||||
|
cp -f x-ui.service.arch ${xui_service}/x-ui.service >/dev/null 2>&1
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
service_installed=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
if [ -f "x-ui.service.rhel" ]; then
|
if [ -f "x-ui.service.rhel" ]; then
|
||||||
echo -e "${green}Found x-ui.service.rhel in extracted files, installing...${plain}"
|
echo -e "${green}Found x-ui.service.rhel in extracted files, installing...${plain}"
|
||||||
|
|
@ -783,6 +846,9 @@ install_x-ui() {
|
||||||
ubuntu | debian | armbian)
|
ubuntu | debian | armbian)
|
||||||
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1
|
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1
|
||||||
;;
|
;;
|
||||||
|
arch | manjaro | parch)
|
||||||
|
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch >/dev/null 2>&1
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1
|
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1
|
||||||
;;
|
;;
|
||||||
|
|
|
||||||
75
update.sh
75
update.sh
|
|
@ -78,7 +78,24 @@ is_ip() {
|
||||||
is_ipv4 "$1" || is_ipv6 "$1"
|
is_ipv4 "$1" || is_ipv6 "$1"
|
||||||
}
|
}
|
||||||
is_domain() {
|
is_domain() {
|
||||||
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1
|
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Port helpers
|
||||||
|
is_port_in_use() {
|
||||||
|
local port="$1"
|
||||||
|
if command -v ss >/dev/null 2>&1; then
|
||||||
|
ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if command -v netstat >/dev/null 2>&1; then
|
||||||
|
netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if command -v lsof >/dev/null 2>&1; then
|
||||||
|
lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
gen_random_string() {
|
gen_random_string() {
|
||||||
|
|
@ -205,7 +222,7 @@ setup_ip_certificate() {
|
||||||
|
|
||||||
echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
|
echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
|
||||||
echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}"
|
echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}"
|
||||||
echo -e "${yellow}Port 80 must be open and accessible from the internet.${plain}"
|
echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}"
|
||||||
|
|
||||||
# Check for acme.sh
|
# Check for acme.sh
|
||||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||||
|
|
@ -241,6 +258,43 @@ setup_ip_certificate() {
|
||||||
# Set reload command for auto-renewal (add || true so it doesn't fail if service stopped)
|
# Set reload command for auto-renewal (add || true so it doesn't fail if service stopped)
|
||||||
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true"
|
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true"
|
||||||
|
|
||||||
|
# Choose port for HTTP-01 listener (default 80, prompt override)
|
||||||
|
local WebPort=""
|
||||||
|
read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort
|
||||||
|
WebPort="${WebPort:-80}"
|
||||||
|
if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then
|
||||||
|
echo -e "${red}Invalid port provided. Falling back to 80.${plain}"
|
||||||
|
WebPort=80
|
||||||
|
fi
|
||||||
|
echo -e "${green}Using port ${WebPort} for standalone validation.${plain}"
|
||||||
|
if [[ "${WebPort}" -ne 80 ]]; then
|
||||||
|
echo -e "${yellow}Reminder: Let's Encrypt still connects on port 80; forward external port 80 to ${WebPort}.${plain}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure chosen port is available
|
||||||
|
while true; do
|
||||||
|
if is_port_in_use "${WebPort}"; then
|
||||||
|
echo -e "${yellow}Port ${WebPort} is currently in use.${plain}"
|
||||||
|
|
||||||
|
local alt_port=""
|
||||||
|
read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port
|
||||||
|
alt_port="${alt_port// /}"
|
||||||
|
if [[ -z "${alt_port}" ]]; then
|
||||||
|
echo -e "${red}Port ${WebPort} is busy; cannot proceed.${plain}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then
|
||||||
|
echo -e "${red}Invalid port provided.${plain}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
WebPort="${alt_port}"
|
||||||
|
continue
|
||||||
|
else
|
||||||
|
echo -e "${green}Port ${WebPort} is free and ready for standalone validation.${plain}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
# Issue certificate with shortlived profile
|
# Issue certificate with shortlived profile
|
||||||
echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}"
|
echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}"
|
||||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1
|
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1
|
||||||
|
|
@ -251,12 +305,12 @@ setup_ip_certificate() {
|
||||||
--server letsencrypt \
|
--server letsencrypt \
|
||||||
--certificate-profile shortlived \
|
--certificate-profile shortlived \
|
||||||
--days 6 \
|
--days 6 \
|
||||||
--httpport 80 \
|
--httpport ${WebPort} \
|
||||||
--force
|
--force
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo -e "${red}Failed to issue IP certificate${plain}"
|
echo -e "${red}Failed to issue IP certificate${plain}"
|
||||||
echo -e "${yellow}Please ensure port 80 is open and accessible from the internet${plain}"
|
echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}"
|
||||||
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
|
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
|
||||||
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
|
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
|
||||||
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
|
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
|
||||||
|
|
@ -683,6 +737,7 @@ update_x-ui() {
|
||||||
rm ${xui_folder} -f >/dev/null 2>&1
|
rm ${xui_folder} -f >/dev/null 2>&1
|
||||||
rm ${xui_folder}/x-ui.service -f >/dev/null 2>&1
|
rm ${xui_folder}/x-ui.service -f >/dev/null 2>&1
|
||||||
rm ${xui_folder}/x-ui.service.debian -f >/dev/null 2>&1
|
rm ${xui_folder}/x-ui.service.debian -f >/dev/null 2>&1
|
||||||
|
rm ${xui_folder}/x-ui.service.arch -f >/dev/null 2>&1
|
||||||
rm ${xui_folder}/x-ui.service.rhel -f >/dev/null 2>&1
|
rm ${xui_folder}/x-ui.service.rhel -f >/dev/null 2>&1
|
||||||
rm ${xui_folder}/x-ui -f >/dev/null 2>&1
|
rm ${xui_folder}/x-ui -f >/dev/null 2>&1
|
||||||
rm ${xui_folder}/x-ui.sh -f >/dev/null 2>&1
|
rm ${xui_folder}/x-ui.sh -f >/dev/null 2>&1
|
||||||
|
|
@ -765,6 +820,15 @@ update_x-ui() {
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
arch | manjaro | parch)
|
||||||
|
if [ -f "x-ui.service.arch" ]; then
|
||||||
|
echo -e "${green}Installing arch-like systemd unit...${plain}"
|
||||||
|
cp -f x-ui.service.arch ${xui_service}/x-ui.service >/dev/null 2>&1
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
service_installed=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
if [ -f "x-ui.service.rhel" ]; then
|
if [ -f "x-ui.service.rhel" ]; then
|
||||||
echo -e "${green}Installing rhel-like systemd unit...${plain}"
|
echo -e "${green}Installing rhel-like systemd unit...${plain}"
|
||||||
|
|
@ -783,6 +847,9 @@ update_x-ui() {
|
||||||
ubuntu | debian | armbian)
|
ubuntu | debian | armbian)
|
||||||
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1
|
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1
|
||||||
;;
|
;;
|
||||||
|
arch | manjaro | parch)
|
||||||
|
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch >/dev/null 2>&1
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1
|
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1
|
||||||
;;
|
;;
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,12 @@ class WebSocketClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.shouldReconnect = true;
|
||||||
|
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
// Ensure basePath ends with '/' for proper URL construction
|
// Ensure basePath ends with '/' for proper URL construction
|
||||||
let basePath = this.basePath || '';
|
let basePath = this.basePath || '';
|
||||||
|
|
@ -97,7 +99,10 @@ class WebSocketClient {
|
||||||
if (!this.listeners.has(event)) {
|
if (!this.listeners.has(event)) {
|
||||||
this.listeners.set(event, []);
|
this.listeners.set(event, []);
|
||||||
}
|
}
|
||||||
this.listeners.get(event).push(callback);
|
const callbacks = this.listeners.get(event);
|
||||||
|
if (!callbacks.includes(callback)) {
|
||||||
|
callbacks.push(callback);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
off(event, callback) {
|
off(event, callback) {
|
||||||
|
|
|
||||||
|
|
@ -1602,7 +1602,6 @@
|
||||||
if (payload && Array.isArray(payload)) {
|
if (payload && Array.isArray(payload)) {
|
||||||
// Use setInbounds to properly convert to DBInbound objects with methods
|
// Use setInbounds to properly convert to DBInbound objects with methods
|
||||||
this.setInbounds(payload);
|
this.setInbounds(payload);
|
||||||
this.searchInbounds(this.searchKey);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1614,14 +1613,31 @@
|
||||||
|
|
||||||
// Update online clients list in real-time
|
// Update online clients list in real-time
|
||||||
if (payload && Array.isArray(payload.onlineClients)) {
|
if (payload && Array.isArray(payload.onlineClients)) {
|
||||||
this.onlineClients = payload.onlineClients;
|
const nextOnlineClients = payload.onlineClients;
|
||||||
// Recalculate client counts to update online status
|
let onlineChanged = this.onlineClients.length !== nextOnlineClients.length;
|
||||||
this.dbInbounds.forEach(dbInbound => {
|
if (!onlineChanged) {
|
||||||
const inbound = this.inbounds.find(ib => ib.id === dbInbound.id);
|
const prevSet = new Set(this.onlineClients);
|
||||||
if (inbound && this.clientCount[dbInbound.id]) {
|
for (const email of nextOnlineClients) {
|
||||||
this.clientCount[dbInbound.id] = this.getClientCounts(dbInbound, inbound);
|
if (!prevSet.has(email)) {
|
||||||
|
onlineChanged = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
this.onlineClients = nextOnlineClients;
|
||||||
|
if (onlineChanged) {
|
||||||
|
// Recalculate client counts to update online status
|
||||||
|
this.dbInbounds.forEach(dbInbound => {
|
||||||
|
const inbound = this.inbounds.find(ib => ib.id === dbInbound.id);
|
||||||
|
if (inbound && this.clientCount[dbInbound.id]) {
|
||||||
|
this.clientCount[dbInbound.id] = this.getClientCounts(dbInbound, inbound);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.enableFilter) {
|
||||||
|
this.filterInbounds();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update last online map in real-time
|
// Update last online map in real-time
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,43 @@
|
||||||
<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
|
<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
|
||||||
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
|
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
|
||||||
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
|
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
|
||||||
|
<style>
|
||||||
|
.subscription-page .subscription-link-box {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 25px 20px 15px 20px;
|
||||||
|
margin-top: -12px;
|
||||||
|
word-break: break-all;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: left;
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark.subscription-page .subscription-link-box {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark.subscription-page .subscription-link-box:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light.subscription-page .subscription-link-box {
|
||||||
|
background: rgba(0, 0, 0, 0.03);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light.subscription-page .subscription-link-box:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
border-color: rgba(0, 0, 0, 0.14);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{{ template "page/head_end" .}}
|
{{ template "page/head_end" .}}
|
||||||
|
|
||||||
{{ template "page/body_start" .}}
|
{{ template "page/body_start" .}}
|
||||||
|
|
@ -138,27 +175,12 @@
|
||||||
style="margin-bottom: -10px; position: relative; z-index: 2; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">
|
style="margin-bottom: -10px; position: relative; z-index: 2; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">
|
||||||
<span>[[ linkName(link, idx) ]]</span>
|
<span>[[ linkName(link, idx) ]]</span>
|
||||||
</a-tag>
|
</a-tag>
|
||||||
<div @click="copy(link)" style="
|
<div @click="copy(link)" class="subscription-link-box">
|
||||||
cursor: pointer;
|
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 25px 20px 15px 20px;
|
|
||||||
margin-top: -12px;
|
|
||||||
word-break: break-all;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
text-align: left;
|
|
||||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
||||||
transition: all 0.3s;
|
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
||||||
" onmouseover="this.style.background='rgba(0, 0, 0, 0.3)'; this.style.borderColor='rgba(255, 255, 255, 0.2)'"
|
|
||||||
onmouseout="this.style.background='rgba(0, 0, 0, 0.2)'; this.style.borderColor='rgba(255, 255, 255, 0.1)'">
|
|
||||||
[[ link ]]
|
[[ link ]]
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,9 @@ func (s *XrayService) GetXrayErr() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
err := p.GetErr()
|
err := p.GetErr()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if runtime.GOOS == "windows" && err.Error() == "exit status 1" {
|
if runtime.GOOS == "windows" && err.Error() == "exit status 1" {
|
||||||
// exit status 1 on Windows means that Xray process was killed
|
// exit status 1 on Windows means that Xray process was killed
|
||||||
|
|
|
||||||
16
x-ui.service.arch
Normal file
16
x-ui.service.arch
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
[Unit]
|
||||||
|
Description=x-ui Service
|
||||||
|
After=network.target
|
||||||
|
Wants=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
EnvironmentFile=-/etc/conf.d/x-ui
|
||||||
|
Environment="XRAY_VMESS_AEAD_FORCED=false"
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/usr/lib/x-ui/
|
||||||
|
ExecStart=/usr/lib/x-ui/x-ui
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
125
x-ui.sh
125
x-ui.sh
|
|
@ -19,6 +19,23 @@ function LOGI() {
|
||||||
echo -e "${green}[INF] $* ${plain}"
|
echo -e "${green}[INF] $* ${plain}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Port helpers: detect listener and owning process (best effort)
|
||||||
|
is_port_in_use() {
|
||||||
|
local port="$1"
|
||||||
|
if command -v ss >/dev/null 2>&1; then
|
||||||
|
ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if command -v netstat >/dev/null 2>&1; then
|
||||||
|
netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if command -v lsof >/dev/null 2>&1; then
|
||||||
|
lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
# Simple helpers for domain/IP validation
|
# Simple helpers for domain/IP validation
|
||||||
is_ipv4() {
|
is_ipv4() {
|
||||||
[[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1
|
[[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1
|
||||||
|
|
@ -30,7 +47,7 @@ is_ip() {
|
||||||
is_ipv4 "$1" || is_ipv6 "$1"
|
is_ipv4 "$1" || is_ipv6 "$1"
|
||||||
}
|
}
|
||||||
is_domain() {
|
is_domain() {
|
||||||
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1
|
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# check root
|
# check root
|
||||||
|
|
@ -539,36 +556,6 @@ enable_bbr() {
|
||||||
before_show_menu
|
before_show_menu
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check the OS and install necessary packages
|
|
||||||
case "${release}" in
|
|
||||||
ubuntu | debian | armbian)
|
|
||||||
apt-get update && apt-get install -yqq --no-install-recommends ca-certificates
|
|
||||||
;;
|
|
||||||
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
|
|
||||||
dnf -y update && dnf -y install ca-certificates
|
|
||||||
;;
|
|
||||||
centos)
|
|
||||||
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
|
||||||
yum -y update && yum -y install ca-certificates
|
|
||||||
else
|
|
||||||
dnf -y update && dnf -y install ca-certificates
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
arch | manjaro | parch)
|
|
||||||
pacman -Sy --noconfirm ca-certificates
|
|
||||||
;;
|
|
||||||
opensuse-tumbleweed | opensuse-leap)
|
|
||||||
zypper refresh && zypper -q install -y ca-certificates
|
|
||||||
;;
|
|
||||||
alpine)
|
|
||||||
apk add ca-certificates
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Enable BBR
|
# Enable BBR
|
||||||
echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf
|
echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf
|
||||||
echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf
|
echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf
|
||||||
|
|
@ -903,24 +890,23 @@ delete_ports() {
|
||||||
}
|
}
|
||||||
|
|
||||||
update_all_geofiles() {
|
update_all_geofiles() {
|
||||||
update_main_geofiles
|
update_geofiles "main"
|
||||||
update_ir_geofiles
|
update_geofiles "IR"
|
||||||
update_ru_geofiles
|
update_geofiles "RU"
|
||||||
}
|
}
|
||||||
|
|
||||||
update_main_geofiles() {
|
update_geofiles() {
|
||||||
curl -fLRo geoip.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
case "${1}" in
|
||||||
curl -fLRo geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
"main") dat_files=(geoip geosite); dat_source="Loyalsoldier/v2ray-rules-dat";;
|
||||||
}
|
"IR") dat_files=(geoip_IR geosite_IR); dat_source="chocolate4u/Iran-v2ray-rules" ;;
|
||||||
|
"RU") dat_files=(geoip_RU geosite_RU); dat_source="runetfreedom/russia-v2ray-rules-dat";;
|
||||||
update_ir_geofiles() {
|
esac
|
||||||
curl -fLRo geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
for dat in "${dat_files[@]}"; do
|
||||||
curl -fLRo geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
# Remove suffix for remote filename (e.g., geoip_IR -> geoip)
|
||||||
}
|
remote_file="${dat%%_*}"
|
||||||
|
curl -fLRo ${xui_folder}/bin/${dat}.dat -z ${xui_folder}/bin/${dat}.dat \
|
||||||
update_ru_geofiles() {
|
https://github.com/${dat_source}/releases/latest/download/${remote_file}.dat
|
||||||
curl -fLRo geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
|
done
|
||||||
curl -fLRo geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update_geo() {
|
update_geo() {
|
||||||
|
|
@ -931,24 +917,22 @@ update_geo() {
|
||||||
echo -e "${green}\t0.${plain} Back to Main Menu"
|
echo -e "${green}\t0.${plain} Back to Main Menu"
|
||||||
read -rp "Choose an option: " choice
|
read -rp "Choose an option: " choice
|
||||||
|
|
||||||
cd ${xui_folder}/bin
|
|
||||||
|
|
||||||
case "$choice" in
|
case "$choice" in
|
||||||
0)
|
0)
|
||||||
show_menu
|
show_menu
|
||||||
;;
|
;;
|
||||||
1)
|
1)
|
||||||
update_main_geofiles
|
update_geofiles "main"
|
||||||
echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}"
|
echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}"
|
||||||
restart
|
restart
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
update_ir_geofiles
|
update_geofiles "IR"
|
||||||
echo -e "${green}chocolate4u datasets have been updated successfully!${plain}"
|
echo -e "${green}chocolate4u datasets have been updated successfully!${plain}"
|
||||||
restart
|
restart
|
||||||
;;
|
;;
|
||||||
3)
|
3)
|
||||||
update_ru_geofiles
|
update_geofiles "RU"
|
||||||
echo -e "${green}runetfreedom datasets have been updated successfully!${plain}"
|
echo -e "${green}runetfreedom datasets have been updated successfully!${plain}"
|
||||||
restart
|
restart
|
||||||
;;
|
;;
|
||||||
|
|
@ -1181,10 +1165,41 @@ ssl_cert_issue_for_ip() {
|
||||||
LOGI "Including IPv6 address: ${ipv6_addr}"
|
LOGI "Including IPv6 address: ${ipv6_addr}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Use port 80 for certificate issuance
|
# Choose port for HTTP-01 listener (default 80, allow override)
|
||||||
local WebPort=80
|
local WebPort=""
|
||||||
|
read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort
|
||||||
|
WebPort="${WebPort:-80}"
|
||||||
|
if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then
|
||||||
|
LOGE "Invalid port provided. Falling back to 80."
|
||||||
|
WebPort=80
|
||||||
|
fi
|
||||||
LOGI "Using port ${WebPort} to issue certificate for IP: ${server_ip}"
|
LOGI "Using port ${WebPort} to issue certificate for IP: ${server_ip}"
|
||||||
LOGI "Make sure port ${WebPort} is open and not in use..."
|
if [[ "${WebPort}" -ne 80 ]]; then
|
||||||
|
LOGI "Reminder: Let's Encrypt still reaches port 80; forward external port 80 to ${WebPort} for validation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if is_port_in_use "${WebPort}"; then
|
||||||
|
LOGI "Port ${WebPort} is currently in use."
|
||||||
|
|
||||||
|
local alt_port=""
|
||||||
|
read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port
|
||||||
|
alt_port="${alt_port// /}"
|
||||||
|
if [[ -z "${alt_port}" ]]; then
|
||||||
|
LOGE "Port ${WebPort} is busy; cannot proceed with issuance."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then
|
||||||
|
LOGE "Invalid port provided."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
WebPort="${alt_port}"
|
||||||
|
continue
|
||||||
|
else
|
||||||
|
LOGI "Port ${WebPort} is free and ready for standalone validation."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
# Reload command - restarts panel after renewal
|
# Reload command - restarts panel after renewal
|
||||||
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null"
|
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue