From ccf08086ac7a921b3b1ace89749c886ca60d1553 Mon Sep 17 00:00:00 2001 From: Nebulosa <85841412+nebulosa2007@users.noreply.github.com> Date: Fri, 9 Jan 2026 19:03:53 +0300 Subject: [PATCH 1/9] refactor update geofiles fuctions (#3653) --- x-ui.sh | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/x-ui.sh b/x-ui.sh index 92d87c14..7a56c357 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -903,24 +903,21 @@ delete_ports() { } update_all_geofiles() { - update_main_geofiles - update_ir_geofiles - update_ru_geofiles + update_geofiles "main" + update_geofiles "IR" + update_geofiles "RU" } -update_main_geofiles() { - curl -fLRo geoip.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat - curl -fLRo geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat -} - -update_ir_geofiles() { - curl -fLRo geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat - curl -fLRo geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat -} - -update_ru_geofiles() { - curl -fLRo geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat - curl -fLRo geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat +update_geofiles() { + case "${1}" in + "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";; + esac + for dat in "${dat_files[@]}"; do + curl -fLRo ${xui_folder}/bin/${dat}.dat -z ${xui_folder}/bin/${dat}.dat \ + https://github.com/${dat_source}/releases/latest/download/${dat%%_}.dat + done } update_geo() { @@ -931,24 +928,22 @@ update_geo() { echo -e "${green}\t0.${plain} Back to Main Menu" read -rp "Choose an option: " choice - cd ${xui_folder}/bin - case "$choice" in 0) show_menu ;; 1) - update_main_geofiles + update_geofiles "main" echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}" restart ;; 2) - update_ir_geofiles + update_geofiles "IR" echo -e "${green}chocolate4u datasets have been updated successfully!${plain}" restart ;; 3) - update_ru_geofiles + update_geofiles "RU" echo -e "${green}runetfreedom datasets have been updated successfully!${plain}" restart ;; From 427b7b67d8c67c75f7a98c84a24145118033ebd8 Mon Sep 17 00:00:00 2001 From: Nebulosa <85841412+nebulosa2007@users.noreply.github.com> Date: Fri, 9 Jan 2026 19:05:55 +0300 Subject: [PATCH 2/9] Refactor ca-certificate dependency (#3655) --- install.sh | 16 ++++++++-------- x-ui.sh | 30 ------------------------------ 2 files changed, 8 insertions(+), 38 deletions(-) diff --git a/install.sh b/install.sh index d0327635..4c68d2dc 100644 --- a/install.sh +++ b/install.sh @@ -59,29 +59,29 @@ is_domain() { install_base() { case "${release}" in 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) - 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) 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 - 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 ;; 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) - zypper refresh && zypper -q install -y curl tar timezone socat + zypper refresh && zypper -q install -y curl tar timezone socat ca-certificates ;; 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 } diff --git a/x-ui.sh b/x-ui.sh index 7a56c357..bdb48817 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -539,36 +539,6 @@ enable_bbr() { before_show_menu 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 echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf From e42c17f2b272c6236af55b948f37c38a66f82e70 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Fri, 9 Jan 2026 20:22:33 +0100 Subject: [PATCH 3/9] Default listen address to 0.0.0.0 in GenXrayInboundConfig When the listen address is empty, it now defaults to 0.0.0.0 to ensure proper dual-stack IPv4/IPv6 binding, improving compatibility on systems with bindv6only=0. --- database/model/model.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/database/model/model.go b/database/model/model.go index 4ca39d87..6225df52 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -80,9 +80,12 @@ type HistoryOfSeeders struct { // GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model. func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { listen := i.Listen - if listen != "" { - listen = fmt.Sprintf("\"%v\"", listen) + // Default to 0.0.0.0 (all interfaces) when listen is empty + // 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{ Listen: json_util.RawMessage(listen), Port: i.Port, From f8c9aac97cfe4bb38c4dad4b1bc5f9bb18a7ec68 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sun, 11 Jan 2026 15:28:43 +0100 Subject: [PATCH 4/9] Add port selection and checks for ACME HTTP-01 listener Introduces user prompts to select the port for ACME HTTP-01 certificate validation (default 80), checks if the chosen port is available, and provides guidance for port forwarding. Adds is_port_in_use helper to all scripts and improves messaging for certificate issuance and error handling. --- install.sh | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++---- update.sh | 60 +++++++++++++++++++++++++++++++++++++++++++++++++--- x-ui.sh | 58 ++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 169 insertions(+), 11 deletions(-) diff --git a/install.sh b/install.sh index 4c68d2dc..59be30ce 100644 --- a/install.sh +++ b/install.sh @@ -53,7 +53,24 @@ is_ip() { is_ipv4 "$1" || is_ipv6 "$1" } 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])*)\.([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() { @@ -180,7 +197,7 @@ setup_ip_certificate() { 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}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 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) 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 echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}" ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1 @@ -226,12 +280,12 @@ setup_ip_certificate() { --server letsencrypt \ --certificate-profile shortlived \ --days 6 \ - --httpport 80 \ + --httpport ${WebPort} \ --force if [ $? -ne 0 ]; then 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 rm -rf ~/.acme.sh/${ipv4} 2>/dev/null [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null diff --git a/update.sh b/update.sh index 9f69f053..800841f5 100755 --- a/update.sh +++ b/update.sh @@ -81,6 +81,23 @@ is_domain() { [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[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() { local length="$1" local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' /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) 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 echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}" ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1 @@ -251,12 +305,12 @@ setup_ip_certificate() { --server letsencrypt \ --certificate-profile shortlived \ --days 6 \ - --httpport 80 \ + --httpport ${WebPort} \ --force if [ $? -ne 0 ]; then 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 rm -rf ~/.acme.sh/${ipv4} 2>/dev/null [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null diff --git a/x-ui.sh b/x-ui.sh index bdb48817..4dda45a0 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -19,6 +19,23 @@ function LOGI() { 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 is_ipv4() { [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1 @@ -885,8 +902,10 @@ update_geofiles() { "RU") dat_files=(geoip_RU geosite_RU); dat_source="runetfreedom/russia-v2ray-rules-dat";; esac for dat in "${dat_files[@]}"; do + # 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 \ - https://github.com/${dat_source}/releases/latest/download/${dat%%_}.dat + https://github.com/${dat_source}/releases/latest/download/${remote_file}.dat done } @@ -1146,10 +1165,41 @@ ssl_cert_issue_for_ip() { LOGI "Including IPv6 address: ${ipv6_addr}" fi - # Use port 80 for certificate issuance - local WebPort=80 + # Choose port for HTTP-01 listener (default 80, allow 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 + LOGE "Invalid port provided. Falling back to 80." + WebPort=80 + fi 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 local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null" From da447e5669c3adaef688e8c361ed2c85426fcdeb Mon Sep 17 00:00:00 2001 From: VolgaIgor <43250768+VolgaIgor@users.noreply.github.com> Date: Sun, 11 Jan 2026 22:18:54 +0300 Subject: [PATCH 5/9] Added curl package to Dockerfile (#3665) --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index ec7a6e7b..bdf877ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,8 @@ RUN apk add --no-cache --update \ ca-certificates \ tzdata \ fail2ban \ - bash + bash \ + curl COPY --from=builder /app/build/ /app/ COPY --from=builder /app/DockerEntrypoint.sh /app/ From a691eaea8dc2b08bba3f24ef4d97fb798204ad45 Mon Sep 17 00:00:00 2001 From: VolgaIgor <43250768+VolgaIgor@users.noreply.github.com> Date: Mon, 12 Jan 2026 04:53:43 +0300 Subject: [PATCH 6/9] Fixed incorrect filtering for IDN top-level domains (#3666) --- install.sh | 2 +- update.sh | 2 +- x-ui.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index 59be30ce..d8e95e22 100644 --- a/install.sh +++ b/install.sh @@ -53,7 +53,7 @@ is_ip() { is_ipv4 "$1" || is_ipv6 "$1" } 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 diff --git a/update.sh b/update.sh index 800841f5..91c37c37 100755 --- a/update.sh +++ b/update.sh @@ -78,7 +78,7 @@ is_ip() { is_ipv4 "$1" || is_ipv6 "$1" } 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 diff --git a/x-ui.sh b/x-ui.sh index 4dda45a0..07aaddc6 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -47,7 +47,7 @@ is_ip() { is_ipv4 "$1" || is_ipv6 "$1" } 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 From 8098d2b1b1c028e4f3d220cc27f43c7a70115a0e Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 13 Jan 2026 17:40:52 +0100 Subject: [PATCH 7/9] Return nil if no error in GetXrayErr Added a check to return nil immediately if p.GetErr() returns nil in GetXrayErr, preventing further error handling when no error is present. --- web/service/xray.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/service/xray.go b/web/service/xray.go index 43178d2f..511ffdda 100644 --- a/web/service/xray.go +++ b/web/service/xray.go @@ -40,6 +40,9 @@ func (s *XrayService) GetXrayErr() error { } err := p.GetErr() + if err == nil { + return nil + } if runtime.GOOS == "windows" && err.Error() == "exit status 1" { // exit status 1 on Windows means that Xray process was killed From 77fa976ee9d6ad529adc96e8826dc56eaedf061d Mon Sep 17 00:00:00 2001 From: lolka1333 Date: Sun, 18 Jan 2026 17:38:57 +0300 Subject: [PATCH 8/9] Enhance WebSocket client connection logic and improve event listener management (#3636) - Updated WebSocketClient to allow connection during CONNECTING state. - Introduced a flag for reconnection attempts. - Improved event listener registration to prevent duplicate callbacks. - Refactored online clients update logic in inbounds.html for better performance and clarity. - Added CSS styles for subscription link boxes in subpage.html to enhance UI consistency and interactivity. Co-authored-by: lolka1333 --- web/assets/js/websocket.js | 9 ++- web/html/inbounds.html | 32 ++++++++--- .../settings/panel/subscription/subpage.html | 56 +++++++++++++------ 3 files changed, 70 insertions(+), 27 deletions(-) diff --git a/web/assets/js/websocket.js b/web/assets/js/websocket.js index 5b8a3948..ccafef87 100644 --- a/web/assets/js/websocket.js +++ b/web/assets/js/websocket.js @@ -14,10 +14,12 @@ class WebSocketClient { } connect() { - if (this.ws && this.ws.readyState === WebSocket.OPEN) { + if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) { return; } + this.shouldReconnect = true; + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; // Ensure basePath ends with '/' for proper URL construction let basePath = this.basePath || ''; @@ -97,7 +99,10 @@ class WebSocketClient { if (!this.listeners.has(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) { diff --git a/web/html/inbounds.html b/web/html/inbounds.html index eeffd98d..b945da90 100644 --- a/web/html/inbounds.html +++ b/web/html/inbounds.html @@ -1602,7 +1602,6 @@ if (payload && Array.isArray(payload)) { // Use setInbounds to properly convert to DBInbound objects with methods this.setInbounds(payload); - this.searchInbounds(this.searchKey); } }); @@ -1614,14 +1613,31 @@ // Update online clients list in real-time if (payload && Array.isArray(payload.onlineClients)) { - this.onlineClients = payload.onlineClients; - // 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); + const nextOnlineClients = payload.onlineClients; + let onlineChanged = this.onlineClients.length !== nextOnlineClients.length; + if (!onlineChanged) { + const prevSet = new Set(this.onlineClients); + for (const email of nextOnlineClients) { + 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 diff --git a/web/html/settings/panel/subscription/subpage.html b/web/html/settings/panel/subscription/subpage.html index 222352ff..c59f68ee 100644 --- a/web/html/settings/panel/subscription/subpage.html +++ b/web/html/settings/panel/subscription/subpage.html @@ -5,6 +5,43 @@ + {{ template "page/head_end" .}} {{ 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);"> [[ linkName(link, idx) ]] -
+
+
From e6318d57e4b737d1921d99278f994a0e76dcd589 Mon Sep 17 00:00:00 2001 From: Nebulosa <85841412+nebulosa2007@users.noreply.github.com> Date: Sun, 18 Jan 2026 17:41:07 +0300 Subject: [PATCH 9/9] Add x-ui.service.arch file (#3650) * Add a service file for Arch-based OSs * Update release.yml with arch service file * Update x-ui.service.arch --- .github/workflows/release.yml | 4 +++- install.sh | 12 ++++++++++++ update.sh | 13 +++++++++++++ x-ui.service.arch | 16 ++++++++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 x-ui.service.arch diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d68ea808..a53ccdc0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,6 +18,7 @@ on: - 'go.mod' - 'go.sum' - 'x-ui.service.debian' + - 'x-ui.service.arch' - 'x-ui.service.rhel' jobs: @@ -80,6 +81,7 @@ jobs: mkdir x-ui cp xui-release 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.sh x-ui/ mv x-ui/xui-release x-ui/x-ui @@ -223,4 +225,4 @@ jobs: file: x-ui-windows-amd64.zip asset_name: x-ui-windows-amd64.zip overwrite: true - prerelease: true \ No newline at end of file + prerelease: true diff --git a/install.sh b/install.sh index d8e95e22..89ddf2a0 100644 --- a/install.sh +++ b/install.sh @@ -818,6 +818,15 @@ install_x-ui() { 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 echo -e "${green}Found x-ui.service.rhel in extracted files, installing...${plain}" @@ -837,6 +846,9 @@ install_x-ui() { 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 ;; + 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 ;; diff --git a/update.sh b/update.sh index 91c37c37..58206984 100755 --- a/update.sh +++ b/update.sh @@ -737,6 +737,7 @@ update_x-ui() { 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.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 -f >/dev/null 2>&1 rm ${xui_folder}/x-ui.sh -f >/dev/null 2>&1 @@ -819,6 +820,15 @@ update_x-ui() { 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 echo -e "${green}Installing rhel-like systemd unit...${plain}" @@ -837,6 +847,9 @@ update_x-ui() { 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 ;; + 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 ;; diff --git a/x-ui.service.arch b/x-ui.service.arch new file mode 100644 index 00000000..b6e01141 --- /dev/null +++ b/x-ui.service.arch @@ -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