diff --git a/config/version b/config/version index adaf203a..d45e55c2 100644 --- a/config/version +++ b/config/version @@ -1 +1 @@ -2.8.6 \ No newline at end of file +2.8.7 \ No newline at end of file diff --git a/go.mod b/go.mod index d8f7789b..475727d9 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/robfig/cron/v3 v3.0.1 github.com/shirou/gopsutil/v4 v4.25.12 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e - github.com/valyala/fasthttp v1.68.0 + github.com/valyala/fasthttp v1.69.0 github.com/xlzd/gotp v0.1.0 github.com/xtls/xray-core v1.251208.0 go.uber.org/atomic v1.11.0 diff --git a/go.sum b/go.sum index e5127e10..70c47d08 100644 --- a/go.sum +++ b/go.sum @@ -191,8 +191,8 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok= -github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4= +github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= +github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM= github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= diff --git a/install.sh b/install.sh index efa4d535..d0327635 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 openssl socat + apt-get update && apt-get install -y -q curl tar tzdata socat ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf -y update && dnf install -y -q curl tar tzdata openssl socat + dnf -y update && dnf install -y -q curl tar tzdata socat ;; centos) if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum -y update && yum install -y curl tar tzdata openssl socat + yum -y update && yum install -y curl tar tzdata socat else - dnf -y update && dnf install -y -q curl tar tzdata openssl socat + dnf -y update && dnf install -y -q curl tar tzdata socat fi ;; arch | manjaro | parch) - pacman -Syu && pacman -Syu --noconfirm curl tar tzdata openssl socat + pacman -Syu && pacman -Syu --noconfirm curl tar tzdata socat ;; opensuse-tumbleweed | opensuse-leap) - zypper refresh && zypper -q install -y curl tar timezone openssl socat + zypper refresh && zypper -q install -y curl tar timezone socat ;; alpine) - apk update && apk add curl tar tzdata openssl socat + apk update && apk add curl tar tzdata socat ;; *) - apt-get update && apt-get install -y -q curl tar tzdata openssl socat + apt-get update && apt-get install -y -q curl tar tzdata socat ;; esac } @@ -154,7 +154,9 @@ setup_ssl_certificate() { # Enable auto-renew ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 - chmod 755 $certPath/* 2>/dev/null + # Secure permissions: private key readable only by owner + chmod 600 $certPath/privkey.pem 2>/dev/null + chmod 644 $certPath/fullchain.pem 2>/dev/null # Set certificate for panel local webCertFile="/root/cert/${domain}/fullchain.pem" @@ -170,56 +172,118 @@ setup_ssl_certificate() { fi } -# Fallback: generate a self-signed certificate (not publicly trusted) -setup_self_signed_certificate() { - local name="$1" # domain or IP to place in SAN - local certDir="/root/cert/selfsigned" +# Issue Let's Encrypt IP certificate with shortlived profile (~6 days validity) +# Requires acme.sh and port 80 open for HTTP-01 challenge +setup_ip_certificate() { + local ipv4="$1" + local ipv6="$2" # optional - echo -e "${yellow}Generating a self-signed certificate (not publicly trusted)...${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}Port 80 must be open and accessible from the internet.${plain}" - mkdir -p "$certDir" - - local sanExt="" - if is_ip "$name"; then - sanExt="IP:${name}" - else - sanExt="DNS:${name}" + # Check for acme.sh + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + install_acme + if [ $? -ne 0 ]; then + echo -e "${red}Failed to install acme.sh${plain}" + return 1 + fi fi - # Use -addext if supported; fallback to config file if needed - openssl req -x509 -nodes -newkey rsa:2048 -days 365 \ - -keyout "${certDir}/privkey.pem" \ - -out "${certDir}/fullchain.pem" \ - -subj "/CN=${name}" \ - -addext "subjectAltName=${sanExt}" >/dev/null 2>&1 - - if [[ $? -ne 0 ]]; then - # Fallback via temporary config file (for older OpenSSL versions) - local tmpCfg="${certDir}/openssl.cnf" - cat > "$tmpCfg" </dev/null 2>&1 - rm -f "$tmpCfg" - fi - - if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then - echo -e "${red}Failed to generate self-signed certificate${plain}" + # Validate IP address + if [[ -z "$ipv4" ]]; then + echo -e "${red}IPv4 address is required${plain}" return 1 fi - chmod 755 ${certDir}/* 2>/dev/null - ${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1 - echo -e "${yellow}Self-signed certificate configured. Browsers will show a warning.${plain}" + if ! is_ipv4 "$ipv4"; then + echo -e "${red}Invalid IPv4 address: $ipv4${plain}" + return 1 + fi + + # Create certificate directory + local certDir="/root/cert/ip" + mkdir -p "$certDir" + + # Build domain arguments + local domain_args="-d ${ipv4}" + if [[ -n "$ipv6" ]] && is_ipv6 "$ipv6"; then + domain_args="${domain_args} -d ${ipv6}" + echo -e "${green}Including IPv6 address: ${ipv6}${plain}" + fi + + # 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" + + # 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 + + ~/.acme.sh/acme.sh --issue \ + ${domain_args} \ + --standalone \ + --server letsencrypt \ + --certificate-profile shortlived \ + --days 6 \ + --httpport 80 \ + --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}" + # 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 + rm -rf ${certDir} 2>/dev/null + return 1 + fi + + echo -e "${green}Certificate issued successfully, installing...${plain}" + + # Install certificate + # Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails, + # but the cert files are still installed. We check for files instead of exit code. + ~/.acme.sh/acme.sh --installcert -d ${ipv4} \ + --key-file "${certDir}/privkey.pem" \ + --fullchain-file "${certDir}/fullchain.pem" \ + --reloadcmd "${reloadCmd}" 2>&1 || true + + # Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero) + if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then + echo -e "${red}Certificate files not found after installation${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 + rm -rf ${certDir} 2>/dev/null + return 1 + fi + + echo -e "${green}Certificate files installed successfully${plain}" + + # Enable auto-upgrade for acme.sh (ensures cron job runs) + ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 + + # Secure permissions: private key readable only by owner + chmod 600 ${certDir}/privkey.pem 2>/dev/null + chmod 644 ${certDir}/fullchain.pem 2>/dev/null + + # Configure panel to use the certificate + echo -e "${green}Setting certificate paths for the panel...${plain}" + ${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" + + if [ $? -ne 0 ]; then + echo -e "${yellow}Warning: Could not set certificate paths automatically${plain}" + echo -e "${yellow}Certificate files are at:${plain}" + echo -e " Cert: ${certDir}/fullchain.pem" + echo -e " Key: ${certDir}/privkey.pem" + else + echo -e "${green}Certificate paths configured successfully${plain}" + fi + + echo -e "${green}IP certificate installed and configured successfully!${plain}" + echo -e "${green}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}" + echo -e "${yellow}acme.sh will automatically renew and reload x-ui before expiry.${plain}" return 0 } @@ -352,14 +416,18 @@ ssl_cert_issue() { if [ $? -ne 0 ]; then echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}" ls -lah /root/cert/${domain}/ - chmod 755 $certPath/* + # Secure permissions: private key readable only by owner + chmod 600 $certPath/privkey.pem 2>/dev/null + chmod 644 $certPath/fullchain.pem 2>/dev/null else echo -e "${green}Auto renew succeeded, certificate details:${plain}" ls -lah /root/cert/${domain}/ - chmod 755 $certPath/* + # Secure permissions: private key readable only by owner + chmod 600 $certPath/privkey.pem 2>/dev/null + chmod 644 $certPath/fullchain.pem 2>/dev/null fi - # Restart panel + # start panel systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null # Prompt user to set panel paths after successful certificate installation @@ -387,7 +455,7 @@ ssl_cert_issue() { return 0 } -# Reusable interactive SSL setup (domain or self-signed) +# Reusable interactive SSL setup (domain or IP) # Sets global `SSL_HOST` to the chosen domain/IP for Access URL usage prompt_and_setup_ssl() { local panel_port="$1" @@ -397,12 +465,13 @@ prompt_and_setup_ssl() { local ssl_choice="" echo -e "${yellow}Choose SSL certificate setup method:${plain}" - echo -e "${green}1.${plain} Let's Encrypt (domain required, recommended)" - echo -e "${green}2.${plain} Self-signed certificate (not publicly trusted)" - read -rp "Choose an option (default 2): " ssl_choice + echo -e "${green}1.${plain} Let's Encrypt for Domain (90-day validity, auto-renews)" + echo -e "${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)" + echo -e "${blue}Note:${plain} Both options require port 80 open. IP certs use shortlived profile." + read -rp "Choose an option (default 2 for IP): " ssl_choice ssl_choice="${ssl_choice// /}" # Trim whitespace - # Default to 2 (self-signed) if not 1 + # Default to 2 (IP cert) if not 1 if [[ "$ssl_choice" != "1" ]]; then ssl_choice="2" fi @@ -410,7 +479,7 @@ prompt_and_setup_ssl() { case "$ssl_choice" in 1) # User chose Let's Encrypt domain option - echo -e "${green}Using ssl_cert_issue() for comprehensive domain setup...${plain}" + echo -e "${green}Using Let's Encrypt for domain certificate...${plain}" ssl_cert_issue # Extract the domain that was used from the certificate local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}') @@ -423,28 +492,30 @@ prompt_and_setup_ssl() { fi ;; 2) - # User chose self-signed option - # Stop panel if running + # User chose Let's Encrypt IP certificate option + echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}" + + # Ask for optional IPv6 + local ipv6_addr="" + read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr + ipv6_addr="${ipv6_addr// /}" # Trim whitespace + + # Stop panel if running (port 80 needed) if [[ $release == "alpine" ]]; then rc-service x-ui stop >/dev/null 2>&1 else systemctl stop x-ui >/dev/null 2>&1 fi - echo -e "${yellow}Using server IP for self-signed certificate: ${server_ip}${plain}" - setup_self_signed_certificate "${server_ip}" + + setup_ip_certificate "${server_ip}" "${ipv6_addr}" if [ $? -eq 0 ]; then SSL_HOST="${server_ip}" - echo -e "${green}✓ Self-signed SSL configured successfully${plain}" + echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}" else - echo -e "${red}✗ Self-signed SSL setup failed${plain}" + echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}" SSL_HOST="${server_ip}" fi - # Start panel after SSL is configured - if [[ $release == "alpine" ]]; then - rc-service x-ui start >/dev/null 2>&1 - else - systemctl start x-ui >/dev/null 2>&1 - fi + ;; *) echo -e "${red}Invalid option. Skipping SSL setup.${plain}" @@ -497,7 +568,7 @@ config_after_install() { echo -e "${green} SSL Certificate Setup (MANDATORY) ${plain}" echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${yellow}For security, SSL certificate is required for all panels.${plain}" - echo -e "${yellow}Let's Encrypt requires a domain name (IP certificates are not issued).${plain}" + echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}" echo "" prompt_and_setup_ssl "${config_port}" "${config_webBasePath}" "${server_ip}" @@ -527,7 +598,7 @@ config_after_install() { echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}" echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${yellow}Let's Encrypt requires a domain name (IP certificates are not issued).${plain}" + echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}" echo "" prompt_and_setup_ssl "${existing_port}" "${config_webBasePath}" "${server_ip}" echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${config_webBasePath}${plain}" @@ -552,7 +623,7 @@ config_after_install() { echo -e "${green}Username, Password, and WebBasePath are properly set.${plain}" fi - # Existing install: if no cert configured, prompt user to set domain or self-signed + # Existing install: if no cert configured, prompt user for SSL setup # Properly detect empty cert by checking if cert: line exists and has content after it existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') if [[ -z "$existing_cert" ]]; then @@ -560,7 +631,7 @@ config_after_install() { echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}" echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${yellow}Let's Encrypt requires a domain name (IP certificates are not issued).${plain}" + echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}" echo "" prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}" echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}" @@ -587,7 +658,7 @@ install_x-ui() { fi fi echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..." - curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz -z ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz + curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz if [[ $? -ne 0 ]]; then echo -e "${red}Downloading x-ui failed, please be sure that your server can access GitHub ${plain}" exit 1 @@ -604,7 +675,7 @@ install_x-ui() { url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz" echo -e "Beginning to install x-ui $1" - curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz -z ${xui_folder}-linux-$(arch).tar.gz ${url} + curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz ${url} if [[ $? -ne 0 ]]; then echo -e "${red}Download x-ui $1 failed, please check if the version exists ${plain}" exit 1 diff --git a/main.go b/main.go index e2af2beb..8096616c 100644 --- a/main.go +++ b/main.go @@ -80,8 +80,8 @@ func runWebServer() { // --- FIX FOR TELEGRAM BOT CONFLICT (409): Stop bot before restart --- service.StopBot() - // -- - + // -- + err := server.Stop() if err != nil { logger.Debug("Error stopping web server:", err) @@ -113,7 +113,7 @@ func runWebServer() { // --- FIX FOR TELEGRAM BOT CONFLICT (409) on full shutdown --- service.StopBot() // ------------------------------------------------------------ - + server.Stop() subServer.Stop() log.Println("Shutting down servers.") diff --git a/sub/subJsonService.go b/sub/subJsonService.go index 86a7a405..8222491a 100644 --- a/sub/subJsonService.go +++ b/sub/subJsonService.go @@ -4,6 +4,7 @@ import ( _ "embed" "encoding/json" "fmt" + "maps" "strings" "github.com/mhsanaei/3x-ui/v2/database/model" @@ -197,9 +198,8 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client, newOutbounds = append(newOutbounds, s.defaultOutbounds...) newConfigJson := make(map[string]any) - for key, value := range s.configJson { - newConfigJson[key] = value - } + maps.Copy(newConfigJson, s.configJson) + newConfigJson["outbounds"] = newOutbounds newConfigJson["remarks"] = s.SubService.genRemark(inbound, client.Email, extPrxy["remark"].(string)) diff --git a/update.sh b/update.sh index dbbdc05c..9f69f053 100755 --- a/update.sh +++ b/update.sh @@ -91,29 +91,29 @@ install_base() { echo -e "${green}Updating and install dependency packages...${plain}" case "${release}" in ubuntu | debian | armbian) - apt-get update >/dev/null 2>&1 && apt-get install -y -q curl tar tzdata openssl socat >/dev/null 2>&1 + apt-get update >/dev/null 2>&1 && apt-get install -y -q curl tar tzdata socat >/dev/null 2>&1 ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata openssl socat >/dev/null 2>&1 + dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata socat >/dev/null 2>&1 ;; centos) if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum -y update >/dev/null 2>&1 && yum install -y -q curl tar tzdata openssl socat >/dev/null 2>&1 + yum -y update >/dev/null 2>&1 && yum install -y -q curl tar tzdata socat >/dev/null 2>&1 else - dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata openssl socat >/dev/null 2>&1 + dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata socat >/dev/null 2>&1 fi ;; arch | manjaro | parch) - pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm curl tar tzdata openssl socat >/dev/null 2>&1 + pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm curl tar tzdata socat >/dev/null 2>&1 ;; opensuse-tumbleweed | opensuse-leap) - zypper refresh >/dev/null 2>&1 && zypper -q install -y curl tar timezone openssl socat >/dev/null 2>&1 + zypper refresh >/dev/null 2>&1 && zypper -q install -y curl tar timezone socat >/dev/null 2>&1 ;; alpine) - apk update >/dev/null 2>&1 && apk add curl tar tzdata openssl socat >/dev/null 2>&1 + apk update >/dev/null 2>&1 && apk add curl tar tzdata socat >/dev/null 2>&1 ;; *) - apt-get update >/dev/null 2>&1 && apt install -y -q curl tar tzdata openssl socat >/dev/null 2>&1 + apt-get update >/dev/null 2>&1 && apt install -y -q curl tar tzdata socat >/dev/null 2>&1 ;; esac } @@ -180,7 +180,8 @@ setup_ssl_certificate() { # Enable auto-renew ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 - chmod 755 $certPath/* 2>/dev/null + chmod 600 $certPath/privkey.pem 2>/dev/null + chmod 644 $certPath/fullchain.pem 2>/dev/null # Set certificate for panel local webCertFile="/root/cert/${domain}/fullchain.pem" @@ -196,57 +197,119 @@ setup_ssl_certificate() { fi } -# Fallback: generate a self-signed certificate (not publicly trusted) -setup_self_signed_certificate() { - local name="$1" # domain or IP to place in SAN - local certDir="/root/cert/selfsigned" +# Issue Let's Encrypt IP certificate with shortlived profile (~6 days validity) +# Requires acme.sh and port 80 open for HTTP-01 challenge +setup_ip_certificate() { + local ipv4="$1" + local ipv6="$2" # optional - echo -e "${yellow}Generating a self-signed certificate (not publicly trusted)...${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}Port 80 must be open and accessible from the internet.${plain}" - mkdir -p "$certDir" - - local sanExt="" - if is_ip "$name"; then - sanExt="IP:${name}" - else - sanExt="DNS:${name}" + # Check for acme.sh + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + install_acme + if [ $? -ne 0 ]; then + echo -e "${red}Failed to install acme.sh${plain}" + return 1 + fi fi - # Try -addext; fallback to config if not supported - openssl req -x509 -nodes -newkey rsa:2048 -days 365 \ - -keyout "${certDir}/privkey.pem" \ - -out "${certDir}/fullchain.pem" \ - -subj "/CN=${name}" \ - -addext "subjectAltName=${sanExt}" >/dev/null 2>&1 - - if [[ $? -ne 0 ]]; then - local tmpCfg="${certDir}/openssl.cnf" - cat > "$tmpCfg" </dev/null 2>&1 - rm -f "$tmpCfg" - fi - - if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then - echo -e "${red}Failed to generate self-signed certificate${plain}" + # Validate IP address + if [[ -z "$ipv4" ]]; then + echo -e "${red}IPv4 address is required${plain}" return 1 fi - chmod 755 ${certDir}/* 2>/dev/null - ${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1 - echo -e "${yellow}Self-signed certificate configured. Browsers will show a warning.${plain}" + if ! is_ipv4 "$ipv4"; then + echo -e "${red}Invalid IPv4 address: $ipv4${plain}" + return 1 + fi + + # Create certificate directory + local certDir="/root/cert/ip" + mkdir -p "$certDir" + + # Build domain arguments + local domain_args="-d ${ipv4}" + if [[ -n "$ipv6" ]] && is_ipv6 "$ipv6"; then + domain_args="${domain_args} -d ${ipv6}" + echo -e "${green}Including IPv6 address: ${ipv6}${plain}" + fi + + # 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" + + # 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 + + ~/.acme.sh/acme.sh --issue \ + ${domain_args} \ + --standalone \ + --server letsencrypt \ + --certificate-profile shortlived \ + --days 6 \ + --httpport 80 \ + --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}" + # 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 + rm -rf ${certDir} 2>/dev/null + return 1 + fi + + echo -e "${green}Certificate issued successfully, installing...${plain}" + + # Install certificate + # Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails, + # but the cert files are still installed. We check for files instead of exit code. + ~/.acme.sh/acme.sh --installcert -d ${ipv4} \ + --key-file "${certDir}/privkey.pem" \ + --fullchain-file "${certDir}/fullchain.pem" \ + --reloadcmd "${reloadCmd}" 2>&1 || true + + # Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero) + if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then + echo -e "${red}Certificate files not found after installation${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 + rm -rf ${certDir} 2>/dev/null + return 1 + fi + + echo -e "${green}Certificate files installed successfully${plain}" + + # Enable auto-upgrade for acme.sh (ensures cron job runs) + ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 + + chmod 600 ${certDir}/privkey.pem 2>/dev/null + chmod 644 ${certDir}/fullchain.pem 2>/dev/null + + # Configure panel to use the certificate + echo -e "${green}Setting certificate paths for the panel...${plain}" + ${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" + if [ $? -ne 0 ]; then + echo -e "${yellow}Warning: Could not set certificate paths automatically.${plain}" + echo -e "${yellow}You may need to set them manually in the panel settings.${plain}" + echo -e "${yellow}Cert path: ${certDir}/fullchain.pem${plain}" + echo -e "${yellow}Key path: ${certDir}/privkey.pem${plain}" + else + echo -e "${green}Certificate paths set successfully!${plain}" + fi + + echo -e "${green}IP certificate installed and configured successfully!${plain}" + echo -e "${green}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}" + echo -e "${yellow}Panel will automatically restart after each renewal.${plain}" return 0 } + # Comprehensive manual SSL certificate issuance via acme.sh ssl_cert_issue() { local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##') @@ -376,11 +439,13 @@ ssl_cert_issue() { if [ $? -ne 0 ]; then echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}" ls -lah /root/cert/${domain}/ - chmod 755 $certPath/* + chmod 600 $certPath/privkey.pem + chmod 644 $certPath/fullchain.pem else echo -e "${green}Auto renew succeeded, certificate details:${plain}" ls -lah /root/cert/${domain}/ - chmod 755 $certPath/* + chmod 600 $certPath/privkey.pem + chmod 644 $certPath/fullchain.pem fi # Restart panel @@ -410,7 +475,7 @@ ssl_cert_issue() { return 0 } -# Unified interactive SSL setup (domain or self-signed) +# Unified interactive SSL setup (domain or IP) # Sets global `SSL_HOST` to the chosen domain/IP prompt_and_setup_ssl() { local panel_port="$1" @@ -420,12 +485,13 @@ prompt_and_setup_ssl() { local ssl_choice="" echo -e "${yellow}Choose SSL certificate setup method:${plain}" - echo -e "${green}1.${plain} Let's Encrypt (domain required, recommended)" - echo -e "${green}2.${plain} Self-signed certificate (for testing/local use)" - read -rp "Choose an option (default 2): " ssl_choice + echo -e "${green}1.${plain} Let's Encrypt for Domain (90-day validity, auto-renews)" + echo -e "${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)" + echo -e "${blue}Note:${plain} Both options require port 80 open. IP certs use shortlived profile." + read -rp "Choose an option (default 2 for IP): " ssl_choice ssl_choice="${ssl_choice// /}" # Trim whitespace - # Default to 2 (self-signed) if not 1 + # Default to 2 (IP cert) if not 1 if [[ "$ssl_choice" != "1" ]]; then ssl_choice="2" fi @@ -433,7 +499,7 @@ prompt_and_setup_ssl() { case "$ssl_choice" in 1) # User chose Let's Encrypt domain option - echo -e "${green}Using ssl_cert_issue() for comprehensive domain setup...${plain}" + echo -e "${green}Using Let's Encrypt for domain certificate...${plain}" ssl_cert_issue # Extract the domain that was used from the certificate local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}') @@ -446,33 +512,37 @@ prompt_and_setup_ssl() { fi ;; 2) - # User chose self-signed option - # Stop panel if running + # User chose Let's Encrypt IP certificate option + echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}" + + # Ask for optional IPv6 + local ipv6_addr="" + read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr + ipv6_addr="${ipv6_addr// /}" # Trim whitespace + + # Stop panel if running (port 80 needed) if [[ $release == "alpine" ]]; then rc-service x-ui stop >/dev/null 2>&1 else systemctl stop x-ui >/dev/null 2>&1 fi - echo -e "${yellow}Using server IP for self-signed certificate: ${server_ip}${plain}" - setup_self_signed_certificate "${server_ip}" + + setup_ip_certificate "${server_ip}" "${ipv6_addr}" if [ $? -eq 0 ]; then SSL_HOST="${server_ip}" - echo -e "${green}✓ Self-signed SSL configured successfully${plain}" + echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}" else - echo -e "${red}✗ Self-signed SSL setup failed${plain}" + echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}" SSL_HOST="${server_ip}" fi - # Start panel after SSL is configured + + # Restart panel after SSL is configured (restart applies new cert settings) if [[ $release == "alpine" ]]; then - rc-service x-ui start >/dev/null 2>&1 + rc-service x-ui restart >/dev/null 2>&1 else - systemctl start x-ui >/dev/null 2>&1 + systemctl restart x-ui >/dev/null 2>&1 fi ;; - 0) - echo -e "${yellow}Skipping SSL setup${plain}" - SSL_HOST="${server_ip}" - ;; *) echo -e "${red}Invalid option. Skipping SSL setup.${plain}" SSL_HOST="${server_ip}" @@ -523,7 +593,7 @@ config_after_update() { echo -e "${red} ⚠ NO SSL CERTIFICATE DETECTED ⚠ ${plain}" echo -e "${red}═══════════════════════════════════════════${plain}" echo -e "${yellow}For security, SSL certificate is MANDATORY for all panels.${plain}" - echo -e "${yellow}Let's Encrypt requires a domain name; IP certs are not issued. Use self-signed for IP.${plain}" + echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}" echo "" if [[ -z "${server_ip}" ]]; then @@ -532,7 +602,7 @@ config_after_update() { return fi - # Prompt and setup SSL (domain or self-signed) + # Prompt and setup SSL (domain or IP) prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}" echo "" @@ -576,10 +646,10 @@ update_x-ui() { fi fi echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..." - ${curl_bin} -fLRo ${xui_folder}-linux-$(arch).tar.gz -z ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null + ${curl_bin} -fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null if [[ $? -ne 0 ]]; then echo -e "${yellow}Trying to fetch version with IPv4...${plain}" - ${curl_bin} -4fLRo ${xui_folder}-linux-$(arch).tar.gz -z ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null + ${curl_bin} -4fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null if [[ $? -ne 0 ]]; then _fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub" fi diff --git a/util/crypto/crypto.go b/util/crypto/crypto.go index 05d088a8..2db5bd83 100644 --- a/util/crypto/crypto.go +++ b/util/crypto/crypto.go @@ -13,6 +13,5 @@ func HashPasswordAsBcrypt(password string) (string, error) { // CheckPasswordHash verifies if the given password matches the bcrypt hash. func CheckPasswordHash(hash, password string) bool { - err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) - return err == nil + return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil } diff --git a/util/ldap/ldap.go b/util/ldap/ldap.go index 795d0e23..1b9faa53 100644 --- a/util/ldap/ldap.go +++ b/util/ldap/ldap.go @@ -24,13 +24,22 @@ type Config struct { // FetchVlessFlags returns map[email]enabled func FetchVlessFlags(cfg Config) (map[string]bool, error) { addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) - var conn *ldap.Conn - var err error + + scheme := "ldap" if cfg.UseTLS { - conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false}) - } else { - conn, err = ldap.Dial("tcp", addr) + scheme = "ldaps" } + + ldapURL := fmt.Sprintf("%s://%s", scheme, addr) + + var opts []ldap.DialOpt + if cfg.UseTLS { + opts = append(opts, ldap.DialWithTLSConfig(&tls.Config{ + InsecureSkipVerify: false, + })) + } + + conn, err := ldap.DialURL(ldapURL, opts...) if err != nil { return nil, err } @@ -91,13 +100,22 @@ func FetchVlessFlags(cfg Config) (map[string]bool, error) { // AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password. func AuthenticateUser(cfg Config, username, password string) (bool, error) { addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) - var conn *ldap.Conn - var err error + + scheme := "ldap" if cfg.UseTLS { - conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false}) - } else { - conn, err = ldap.Dial("tcp", addr) + scheme = "ldaps" } + + ldapURL := fmt.Sprintf("%s://%s", scheme, addr) + + var opts []ldap.DialOpt + if cfg.UseTLS { + opts = append(opts, ldap.DialWithTLSConfig(&tls.Config{ + InsecureSkipVerify: false, + })) + } + + conn, err := ldap.DialURL(ldapURL, opts...) if err != nil { return false, err } diff --git a/util/random/random.go b/util/random/random.go index c746df63..ddb819c1 100644 --- a/util/random/random.go +++ b/util/random/random.go @@ -18,10 +18,10 @@ var ( // init initializes the character sequences used for random string generation. // It sets up arrays for numbers, lowercase letters, uppercase letters, and combinations. func init() { - for i := 0; i < 10; i++ { + for i := range 10 { numSeq[i] = rune('0' + i) } - for i := 0; i < 26; i++ { + for i := range 26 { lowerSeq[i] = rune('a' + i) upperSeq[i] = rune('A' + i) } @@ -40,7 +40,7 @@ func init() { // Seq generates a random string of length n containing alphanumeric characters (numbers, lowercase and uppercase letters). func Seq(n int) string { runes := make([]rune, n) - for i := 0; i < n; i++ { + for i := range n { idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(allSeq)))) if err != nil { panic("crypto/rand failed: " + err.Error()) diff --git a/util/reflect_util/reflect.go b/util/reflect_util/reflect.go index 1f557e0d..b56f1456 100644 --- a/util/reflect_util/reflect.go +++ b/util/reflect_util/reflect.go @@ -7,7 +7,7 @@ import "reflect" func GetFields(t reflect.Type) []reflect.StructField { num := t.NumField() fields := make([]reflect.StructField, 0, num) - for i := 0; i < num; i++ { + for i := range num { fields = append(fields, t.Field(i)) } return fields @@ -17,7 +17,7 @@ func GetFields(t reflect.Type) []reflect.StructField { func GetFieldValues(v reflect.Value) []reflect.Value { num := v.NumField() fields := make([]reflect.Value, 0, num) - for i := 0; i < num; i++ { + for i := range num { fields = append(fields, v.Field(i)) } return fields diff --git a/util/sys/sys_darwin.go b/util/sys/sys_darwin.go index 6ecc78c0..b635b549 100644 --- a/util/sys/sys_darwin.go +++ b/util/sys/sys_darwin.go @@ -47,11 +47,11 @@ func CPUPercentRaw() (float64, error) { var out [5]uint64 switch len(raw) { case 5 * 8: - for i := 0; i < 5; i++ { + for i := range 5 { out[i] = binary.LittleEndian.Uint64(raw[i*8 : (i+1)*8]) } case 5 * 4: - for i := 0; i < 5; i++ { + for i := range 5 { out[i] = uint64(binary.LittleEndian.Uint32(raw[i*4 : (i+1)*4])) } default: diff --git a/web/assets/js/model/inbound.js b/web/assets/js/model/inbound.js index eac057a4..9aa05ed3 100644 --- a/web/assets/js/model/inbound.js +++ b/web/assets/js/model/inbound.js @@ -317,7 +317,7 @@ TcpStreamSettings.TcpResponse = class extends XrayCommonClass { class KcpStreamSettings extends XrayCommonClass { constructor( - mtu = 1350, + mtu = 1250, tti = 50, uplinkCapacity = 5, downlinkCapacity = 20, @@ -2644,7 +2644,7 @@ Inbound.HttpSettings.HttpAccount = class extends XrayCommonClass { Inbound.WireguardSettings = class extends XrayCommonClass { constructor( protocol, - mtu = 1420, + mtu = 1250, secretKey = Wireguard.generateKeypair().privateKey, peers = [new Inbound.WireguardSettings.Peer()], noKernelTun = false diff --git a/web/assets/js/model/outbound.js b/web/assets/js/model/outbound.js index 295ac812..6fe34982 100644 --- a/web/assets/js/model/outbound.js +++ b/web/assets/js/model/outbound.js @@ -164,7 +164,7 @@ class TcpStreamSettings extends CommonClass { class KcpStreamSettings extends CommonClass { constructor( - mtu = 1350, + mtu = 1250, tti = 50, uplinkCapacity = 5, downlinkCapacity = 20, @@ -1233,7 +1233,7 @@ Outbound.HttpSettings = class extends CommonClass { Outbound.WireguardSettings = class extends CommonClass { constructor( - mtu = 1420, + mtu = 1250, secretKey = '', address = [''], workers = 2, diff --git a/web/assets/js/subscription.js b/web/assets/js/subscription.js index c7627837..b79d361c 100644 --- a/web/assets/js/subscription.js +++ b/web/assets/js/subscription.js @@ -138,14 +138,14 @@ return `streisand://import/${encodeURIComponent(this.app.subUrl)}`; }, v2raytunUrl() { - return this.app.subUrl; + return this.app.subUrl; }, npvtunUrl() { - return this.app.subUrl; + return this.app.subUrl; }, - happUrl() { - return `happ://add/${encodeURIComponent(this.app.subUrl)}`; - } + happUrl() { + return `happ://add/${encodeURIComponent(this.app.subUrl)}`; + } }, methods: { renderLink, diff --git a/web/controller/server.go b/web/controller/server.go index 5b39700e..d32209e1 100644 --- a/web/controller/server.go +++ b/web/controller/server.go @@ -210,10 +210,10 @@ func (a *ServerController) getXrayLogs(c *gin.Context) { //getting tags for freedom and blackhole outbounds config, err := a.settingService.GetDefaultXrayConfig() if err == nil && config != nil { - if cfgMap, ok := config.(map[string]interface{}); ok { - if outbounds, ok := cfgMap["outbounds"].([]interface{}); ok { + if cfgMap, ok := config.(map[string]any); ok { + if outbounds, ok := cfgMap["outbounds"].([]any); ok { for _, outbound := range outbounds { - if obMap, ok := outbound.(map[string]interface{}); ok { + if obMap, ok := outbound.(map[string]any); ok { switch obMap["protocol"] { case "freedom": if tag, ok := obMap["tag"].(string); ok { diff --git a/web/global/global.go b/web/global/global.go index f72c7bfe..5556b486 100644 --- a/web/global/global.go +++ b/web/global/global.go @@ -17,7 +17,7 @@ var ( type WebServer interface { GetCron() *cron.Cron // Get the cron scheduler GetCtx() context.Context // Get the server context - GetWSHub() interface{} // Get the WebSocket hub (using interface{} to avoid circular dependency) + GetWSHub() any // Get the WebSocket hub (using any to avoid circular dependency) } // SubServer interface defines methods for accessing the subscription server instance. diff --git a/web/html/common/page.html b/web/html/common/page.html index 0af63afb..058682d5 100644 --- a/web/html/common/page.html +++ b/web/html/common/page.html @@ -24,6 +24,40 @@ body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; } + + /* mobile touch scrolling for tabs */ + @media (max-width: 576px) { + .ant-tabs-nav-container { + overflow-x: auto !important; + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; + overscroll-behavior-x: contain; + white-space: nowrap; + max-width: 100%; + padding: 0 !important; /* Remove padding for arrows */ + } + .ant-tabs-nav-wrap { + overflow: visible !important; + padding: 0 !important; + } + .ant-tabs-nav-scroll { + overflow: visible !important; + box-shadow: none !important; + } + .ant-tabs-nav { + display: flex !important; + transform: none !important; /* Disable JS transform */ + width: auto !important; + margin: 0 !important; + } + .ant-tabs-tab-prev, + .ant-tabs-tab-next { + display: none !important; /* Hide arrows */ + } + .ant-tabs-nav-container::-webkit-scrollbar { + display: none; + } + } {{ .host }} – {{ i18n .title}} {{ end }} diff --git a/web/html/form/protocol/vless.html b/web/html/form/protocol/vless.html index bdf75be3..fc9c3852 100644 --- a/web/html/form/protocol/vless.html +++ b/web/html/form/protocol/vless.html @@ -1,6 +1,5 @@ {{define "form/vless"}} - + {{template "form/client"}} @@ -22,115 +21,103 @@ - + - + + + Fallback [[ index + 1 ]] + + + + + + + + + + + + + + + + + + + + {{end}} \ No newline at end of file diff --git a/web/html/inbounds.html b/web/html/inbounds.html index 2cc2801a..a6143896 100644 --- a/web/html/inbounds.html +++ b/web/html/inbounds.html @@ -1711,24 +1711,9 @@ // Listen for traffic updates window.wsClient.on('traffic', (payload) => { - if (payload && payload.clientTraffics && Array.isArray(payload.clientTraffics)) { - // Update client traffic statistics - payload.clientTraffics.forEach(clientTraffic => { - const dbInbound = this.dbInbounds.find(ib => { - if (!ib) return false; - const clients = this.getInboundClients(ib); - return clients && Array.isArray(clients) && clients.some(c => c && c.email === clientTraffic.email); - }); - if (dbInbound && dbInbound.clientStats && Array.isArray(dbInbound.clientStats)) { - const stats = dbInbound.clientStats.find(s => s && s.email === clientTraffic.email); - if (stats) { - stats.up = clientTraffic.up || stats.up; - stats.down = clientTraffic.down || stats.down; - stats.total = clientTraffic.total || stats.total; - } - } - }); - } + // Note: Do NOT update total consumed traffic (stats.up, stats.down) from this event + // because clientTraffics contains delta/incremental values, not total accumulated values. + // Total traffic is updated via the 'inbounds' event which contains accumulated values from database. // Update online clients list in real-time if (payload && Array.isArray(payload.onlineClients)) { @@ -1748,8 +1733,6 @@ } }); - // Notifications disabled - white notifications are not needed - // Fallback to polling if WebSocket fails window.wsClient.on('error', () => { console.warn('WebSocket connection failed, falling back to polling'); diff --git a/web/html/settings/panel/subscription/subpage.html b/web/html/settings/panel/subscription/subpage.html index 0043d0d2..222352ff 100644 --- a/web/html/settings/panel/subscription/subpage.html +++ b/web/html/settings/panel/subscription/subpage.html @@ -20,28 +20,20 @@