From 9b51e9a5c5c087139454fca56ae5f375f40a017c Mon Sep 17 00:00:00 2001 From: mhsanaei Date: Thu, 14 Aug 2025 18:38:56 +0200 Subject: [PATCH 01/27] Freedom: Add maxSplit fragment option; Add applyTo noises option --- web/assets/js/model/outbound.js | 11 +++++++++-- web/html/form/outbound.html | 8 ++++++++ web/html/settings.html | 9 +++++++-- web/html/settings/panel/subscription/json.html | 12 ++++++++++++ 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/web/assets/js/model/outbound.js b/web/assets/js/model/outbound.js index a42c400d..ce110a73 100644 --- a/web/assets/js/model/outbound.js +++ b/web/assets/js/model/outbound.js @@ -919,12 +919,14 @@ Outbound.FreedomSettings.Fragment = class extends CommonClass { constructor( packets = '1-3', length = '', - interval = '' + interval = '', + maxSplit = '' ) { super(); this.packets = packets; this.length = length; this.interval = interval; + this.maxSplit = maxSplit; } static fromJson(json = {}) { @@ -932,6 +934,7 @@ Outbound.FreedomSettings.Fragment = class extends CommonClass { json.packets, json.length, json.interval, + json.maxSplit ); } }; @@ -940,12 +943,14 @@ Outbound.FreedomSettings.Noise = class extends CommonClass { constructor( type = 'rand', packet = '10-20', - delay = '10-16' + delay = '10-16', + applyTo = 'ip' ) { super(); this.type = type; this.packet = packet; this.delay = delay; + this.applyTo = applyTo; } static fromJson(json = {}) { @@ -953,6 +958,7 @@ Outbound.FreedomSettings.Noise = class extends CommonClass { json.type, json.packet, json.delay, + json.applyTo ); } @@ -961,6 +967,7 @@ Outbound.FreedomSettings.Noise = class extends CommonClass { type: this.type, packet: this.packet, delay: this.delay, + applyTo: this.applyTo }; } }; diff --git a/web/html/form/outbound.html b/web/html/form/outbound.html index eed7316d..0b75ba27 100644 --- a/web/html/form/outbound.html +++ b/web/html/form/outbound.html @@ -42,6 +42,9 @@ + + + @@ -75,6 +78,11 @@ + + + [[ s ]] + + diff --git a/web/html/settings.html b/web/html/settings.html index c7fa9bd8..3b6e81a1 100644 --- a/web/html/settings.html +++ b/web/html/settings.html @@ -207,7 +207,7 @@ settings: { domainStrategy: "AsIs", noises: [ - { type: "rand", packet: "10-20", delay: "10-16" }, + { type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" }, ], }, }, @@ -397,7 +397,7 @@ } }, addNoise() { - const newNoise = { type: "rand", packet: "10-20", delay: "10-16" }; + const newNoise = { type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" }; this.noisesArray = [...this.noisesArray, newNoise]; }, removeNoise(index) { @@ -420,6 +420,11 @@ updatedNoises[index] = { ...updatedNoises[index], delay: value }; this.noisesArray = updatedNoises; }, + updateNoiseApplyTo(index, value) { + const updatedNoises = [...this.noisesArray]; + updatedNoises[index] = { ...updatedNoises[index], applyTo: value }; + this.noisesArray = updatedNoises; + }, }, computed: { fragment: { diff --git a/web/html/settings/panel/subscription/json.html b/web/html/settings/panel/subscription/json.html index a3729984..c8575e38 100644 --- a/web/html/settings/panel/subscription/json.html +++ b/web/html/settings/panel/subscription/json.html @@ -90,6 +90,18 @@ placeholder="10-20"> + + + + Remove From 978755960fccaa4b26d4f2ab225d10092c031e16 Mon Sep 17 00:00:00 2001 From: mhsanaei Date: Thu, 14 Aug 2025 18:41:53 +0200 Subject: [PATCH 02/27] actions/checkout from 4 to 5 --- .github/workflows/docker.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ce3a94f0..0e460d24 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 358e9029..c43f6b4b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Go uses: actions/setup-go@v5 From 3d0212c21dead086c993e3c9885836325b0b1c06 Mon Sep 17 00:00:00 2001 From: mhsanaei Date: Fri, 15 Aug 2025 13:33:31 +0200 Subject: [PATCH 03/27] fix: fail2ban on Debian 12 #1701 --- install.sh | 2 +- x-ui.sh | 230 ++++++++++++++++++++++++++++------------------------- 2 files changed, 121 insertions(+), 111 deletions(-) diff --git a/install.sh b/install.sh index d3e6dd1b..ce59abe6 100644 --- a/install.sh +++ b/install.sh @@ -58,7 +58,7 @@ install_base() { zypper refresh && zypper -q install -y wget curl tar timezone ;; *) - apt-get update && apt install -y -q wget curl tar tzdata + apt-get update && apt-get install -y -q wget curl tar tzdata ;; esac } diff --git a/x-ui.sh b/x-ui.sh index 97dc9102..4acf2dd6 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -398,37 +398,6 @@ show_log() { esac } -show_banlog() { - local system_log="/var/log/fail2ban.log" - - echo -e "${green}Checking ban logs...${plain}\n" - - if ! systemctl is-active --quiet fail2ban; then - echo -e "${red}Fail2ban service is not running!${plain}\n" - return 1 - fi - - if [[ -f "$system_log" ]]; then - echo -e "${green}Recent system ban activities from fail2ban.log:${plain}" - grep "3x-ipl" "$system_log" | grep -E "Ban|Unban" | tail -n 10 || echo -e "${yellow}No recent system ban activities found${plain}" - echo "" - fi - - if [[ -f "${iplimit_banned_log_path}" ]]; then - echo -e "${green}3X-IPL ban log entries:${plain}" - if [[ -s "${iplimit_banned_log_path}" ]]; then - grep -v "INIT" "${iplimit_banned_log_path}" | tail -n 10 || echo -e "${yellow}No ban entries found${plain}" - else - echo -e "${yellow}Ban log file is empty${plain}" - fi - else - echo -e "${red}Ban log file not found at: ${iplimit_banned_log_path}${plain}" - fi - - echo -e "\n${green}Current jail status:${plain}" - fail2ban-client status 3x-ipl || echo -e "${yellow}Unable to get jail status${plain}" -} - bbr_menu() { echo -e "${green}\t1.${plain} Enable BBR" echo -e "${green}\t2.${plain} Disable BBR" @@ -1005,7 +974,7 @@ ssl_cert_issue() { # install socat second case "${release}" in ubuntu | debian | armbian) - apt update && apt install socat -y + apt-get update && apt-get install socat -y ;; centos | rhel | almalinux | rocky | ol) yum -y update && yum -y install socat @@ -1330,81 +1299,7 @@ run_speedtest() { speedtest } -create_iplimit_jails() { - # Use default bantime if not passed => 30 minutes - local bantime="${1:-30}" - # Uncomment 'allowipv6 = auto' in fail2ban.conf - sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf - - # On Debian 12+ fail2ban's default backend should be changed to systemd - if [[ "${release}" == "debian" && ${os_version} -ge 12 ]]; then - sed -i '0,/action =/s/backend = auto/backend = systemd/' /etc/fail2ban/jail.conf - fi - - cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf -[3x-ipl] -enabled=true -backend=auto -filter=3x-ipl -action=3x-ipl -logpath=${iplimit_log_path} -maxretry=2 -findtime=32 -bantime=${bantime}m -EOF - - cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf -[Definition] -datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S -failregex = \[LIMIT_IP\]\s*Email\s*=\s*.+\s*\|\|\s*SRC\s*=\s* -ignoreregex = -EOF - - cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf -[INCLUDES] -before = iptables-allports.conf - -[Definition] -actionstart = -N f2b- - -A f2b- -j - -I -p -j f2b- - -actionstop = -D -p -j f2b- - - -X f2b- - -actioncheck = -n -L | grep -q 'f2b-[ \t]' - -actionban = -I f2b- 1 -s -j - echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = [IP] = banned for seconds." >> ${iplimit_banned_log_path} - -actionunban = -D f2b- -s -j - echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = [IP] = unbanned." >> ${iplimit_banned_log_path} - -[Init] -name = default -protocol = tcp -chain = INPUT -EOF - - echo -e "${green}Ip Limit jail files created with a bantime of ${bantime} minutes.${plain}" -} - -iplimit_remove_conflicts() { - local jail_files=( - /etc/fail2ban/jail.conf - /etc/fail2ban/jail.local - ) - - for file in "${jail_files[@]}"; do - # Check for [3x-ipl] config in jail file then remove it - if test -f "${file}" && grep -qw '3x-ipl' ${file}; then - sed -i "/\[3x-ipl\]/,/^$/d" ${file} - echo -e "${yellow}Removing conflicts of [3x-ipl] in jail (${file})!${plain}\n" - fi - done -} ip_validation() { ipv6_regex="^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$" @@ -1514,14 +1409,22 @@ install_iplimit() { # Check the OS and install necessary packages case "${release}" in ubuntu) + apt-get update if [[ "${os_version}" -ge 24 ]]; then - apt update && apt install python3-pip -y + apt-get install python3-pip -y python3 -m pip install pyasynchat --break-system-packages fi - apt update && apt install fail2ban -y + apt-get install fail2ban -y ;; - debian | armbian) - apt update && apt install fail2ban -y + debian) + apt-get update + if [ "$os_version" -ge 12 ]; then + apt-get install -y python3-systemd + fi + apt-get install -y fail2ban + ;; + armbian) + apt-get update && apt-get install fail2ban -y ;; centos | rhel | almalinux | rocky | ol) yum update -y && yum install epel-release -y @@ -1632,6 +1535,113 @@ remove_iplimit() { esac } +show_banlog() { + local system_log="/var/log/fail2ban.log" + + echo -e "${green}Checking ban logs...${plain}\n" + + if ! systemctl is-active --quiet fail2ban; then + echo -e "${red}Fail2ban service is not running!${plain}\n" + return 1 + fi + + if [[ -f "$system_log" ]]; then + echo -e "${green}Recent system ban activities from fail2ban.log:${plain}" + grep "3x-ipl" "$system_log" | grep -E "Ban|Unban" | tail -n 10 || echo -e "${yellow}No recent system ban activities found${plain}" + echo "" + fi + + if [[ -f "${iplimit_banned_log_path}" ]]; then + echo -e "${green}3X-IPL ban log entries:${plain}" + if [[ -s "${iplimit_banned_log_path}" ]]; then + grep -v "INIT" "${iplimit_banned_log_path}" | tail -n 10 || echo -e "${yellow}No ban entries found${plain}" + else + echo -e "${yellow}Ban log file is empty${plain}" + fi + else + echo -e "${red}Ban log file not found at: ${iplimit_banned_log_path}${plain}" + fi + + echo -e "\n${green}Current jail status:${plain}" + fail2ban-client status 3x-ipl || echo -e "${yellow}Unable to get jail status${plain}" +} + +create_iplimit_jails() { + # Use default bantime if not passed => 30 minutes + local bantime="${1:-30}" + + # Uncomment 'allowipv6 = auto' in fail2ban.conf + sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf + + # On Debian 12+ fail2ban's default backend should be changed to systemd + if [[ "${release}" == "debian" && ${os_version} -ge 12 ]]; then + sed -i '0,/action =/s/backend = auto/backend = systemd/' /etc/fail2ban/jail.conf + fi + + cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf +[3x-ipl] +enabled=true +backend=auto +filter=3x-ipl +action=3x-ipl +logpath=${iplimit_log_path} +maxretry=2 +findtime=32 +bantime=${bantime}m +EOF + + cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf +[Definition] +datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S +failregex = \[LIMIT_IP\]\s*Email\s*=\s*.+\s*\|\|\s*SRC\s*=\s* +ignoreregex = +EOF + + cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf +[INCLUDES] +before = iptables-allports.conf + +[Definition] +actionstart = -N f2b- + -A f2b- -j + -I -p -j f2b- + +actionstop = -D -p -j f2b- + + -X f2b- + +actioncheck = -n -L | grep -q 'f2b-[ \t]' + +actionban = -I f2b- 1 -s -j + echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = [IP] = banned for seconds." >> ${iplimit_banned_log_path} + +actionunban = -D f2b- -s -j + echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = [IP] = unbanned." >> ${iplimit_banned_log_path} + +[Init] +name = default +protocol = tcp +chain = INPUT +EOF + + echo -e "${green}Ip Limit jail files created with a bantime of ${bantime} minutes.${plain}" +} + +iplimit_remove_conflicts() { + local jail_files=( + /etc/fail2ban/jail.conf + /etc/fail2ban/jail.local + ) + + for file in "${jail_files[@]}"; do + # Check for [3x-ipl] config in jail file then remove it + if test -f "${file}" && grep -qw '3x-ipl' ${file}; then + sed -i "/\[3x-ipl\]/,/^$/d" ${file} + echo -e "${yellow}Removing conflicts of [3x-ipl] in jail (${file})!${plain}\n" + fi + done +} + SSH_port_forwarding() { local server_ip=$(curl -s --max-time 3 https://api.ipify.org) if [ -z "$server_ip" ]; then From 27445b30e9675d17f7da4ca00bd7911bc0e8571d Mon Sep 17 00:00:00 2001 From: mhsanaei Date: Sun, 17 Aug 2025 12:22:33 +0200 Subject: [PATCH 04/27] DNS outbound: Set "reject" as the default value for nonIPQuery --- web/assets/js/model/outbound.js | 2 +- web/html/form/outbound.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/assets/js/model/outbound.js b/web/assets/js/model/outbound.js index ce110a73..ee78795f 100644 --- a/web/assets/js/model/outbound.js +++ b/web/assets/js/model/outbound.js @@ -995,7 +995,7 @@ Outbound.DNSSettings = class extends CommonClass { network = 'udp', address = '', port = 53, - nonIPQuery = 'drop', + nonIPQuery = 'reject', blockTypes = [] ) { super(); diff --git a/web/html/form/outbound.html b/web/html/form/outbound.html index 0b75ba27..c7a786b7 100644 --- a/web/html/form/outbound.html +++ b/web/html/form/outbound.html @@ -105,7 +105,7 @@ - [[ s ]] + [[ s ]] From 16f53ce4c2c67997e94dd39483487eb64eac5e25 Mon Sep 17 00:00:00 2001 From: mhsanaei Date: Sun, 17 Aug 2025 12:27:21 +0200 Subject: [PATCH 05/27] go v1.25 --- Dockerfile | 2 +- go.mod | 8 ++++---- go.sum | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4a15f98d..b818a7cd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # ======================================================== # Stage: Builder # ======================================================== -FROM golang:1.24-alpine AS builder +FROM golang:1.25-alpine AS builder WORKDIR /app ARG TARGETARCH diff --git a/go.mod b/go.mod index 32b6856c..295bee9b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module x-ui -go 1.24.5 +go 1.25.0 require ( github.com/gin-contrib/gzip v1.2.3 @@ -15,7 +15,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 github.com/robfig/cron/v3 v3.0.1 github.com/shirou/gopsutil/v4 v4.25.7 - github.com/valyala/fasthttp v1.64.0 + github.com/valyala/fasthttp v1.65.0 github.com/xlzd/gotp v0.1.0 github.com/xtls/xray-core v1.250803.0 go.uber.org/atomic v1.11.0 @@ -56,7 +56,7 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.30 // indirect + github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/miekg/dns v1.1.68 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -92,7 +92,7 @@ require ( golang.org/x/tools v0.36.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect google.golang.org/protobuf v1.36.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect diff --git a/go.sum b/go.sum index f11a3071..1fdbdd83 100644 --- a/go.sum +++ b/go.sum @@ -95,8 +95,8 @@ github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr32 github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSYYCY= -github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -162,8 +162,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.64.0 h1:QBygLLQmiAyiXuRhthf0tuRkqAFcrC42dckN2S+N3og= -github.com/valyala/fasthttp v1.64.0/go.mod h1:dGmFxwkWXSK0NbOSJuF7AMVzU+lkHz0wQVvVITv2UQA= +github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8= +github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= @@ -226,8 +226,8 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a h1:tPE/Kp+x9dMSwUm/uM0JKK0IfdiJkwAbSMSeZBXXJXc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo= google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= From 6b23b416a788aedeacd75e445f8b1a919c2cf5c8 Mon Sep 17 00:00:00 2001 From: mhsanaei Date: Sun, 17 Aug 2025 13:37:49 +0200 Subject: [PATCH 06/27] minor changes --- sub/subJsonService.go | 5 ++- web/entity/entity.go | 6 +-- web/job/check_client_ip_job.go | 11 ++--- web/service/inbound.go | 56 ++++++++++++++---------- web/service/tgbot.go | 80 +++++++++++++++++++++++++--------- 5 files changed, 100 insertions(+), 58 deletions(-) diff --git a/sub/subJsonService.go b/sub/subJsonService.go index 7bc4d1db..680a01c0 100644 --- a/sub/subJsonService.go +++ b/sub/subJsonService.go @@ -209,9 +209,10 @@ func (s *SubJsonService) streamData(stream string) map[string]any { var streamSettings map[string]any json.Unmarshal([]byte(stream), &streamSettings) security, _ := streamSettings["security"].(string) - if security == "tls" { + switch security { + case "tls": streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]any)) - } else if security == "reality" { + case "reality": streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]any)) } delete(streamSettings, "sockopt") diff --git a/web/entity/entity.go b/web/entity/entity.go index 889c9024..844a7ce0 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -2,10 +2,10 @@ package entity import ( "crypto/tls" + "math" "net" "strings" "time" - "math" "x-ui/util/common" ) @@ -39,8 +39,8 @@ type AllSetting struct { TgCpu int `json:"tgCpu" form:"tgCpu"` TgLang string `json:"tgLang" form:"tgLang"` TimeLocation string `json:"timeLocation" form:"timeLocation"` - TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"` - TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"` + TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"` + TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"` SubEnable bool `json:"subEnable" form:"subEnable"` SubTitle string `json:"subTitle" form:"subTitle"` SubListen string `json:"subListen" form:"subListen"` diff --git a/web/job/check_client_ip_job.go b/web/job/check_client_ip_job.go index b95c8ee2..5a30b616 100644 --- a/web/job/check_client_ip_job.go +++ b/web/job/check_client_ip_job.go @@ -11,7 +11,6 @@ import ( "sort" "time" - "slices" "x-ui/database" "x-ui/database/model" "x-ui/logger" @@ -58,21 +57,21 @@ func (j *CheckClientIpJob) Run() { func (j *CheckClientIpJob) clearAccessLog() { logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) j.checkError(err) + defer logAccessP.Close() accessLogPath, err := xray.GetAccessLogPath() j.checkError(err) file, err := os.Open(accessLogPath) j.checkError(err) + defer file.Close() _, err = io.Copy(logAccessP, file) j.checkError(err) - logAccessP.Close() - file.Close() - err = os.Truncate(accessLogPath, 0) j.checkError(err) + j.lastClear = time.Now().Unix() } @@ -193,10 +192,6 @@ func (j *CheckClientIpJob) checkError(e error) { } } -func (j *CheckClientIpJob) contains(s []string, str string) bool { - return slices.Contains(s, str) -} - func (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.InboundClientIps, error) { db := database.GetDB() InboundClientIps := &model.InboundClientIps{} diff --git a/web/service/inbound.go b/web/service/inbound.go index 66e1a420..6e10e798 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -177,15 +177,16 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo // Secure client ID for _, client := range clients { - if inbound.Protocol == "trojan" { + switch inbound.Protocol { + case "trojan": if client.Password == "" { return inbound, false, common.NewError("empty client ID") } - } else if inbound.Protocol == "shadowsocks" { + case "shadowsocks": if client.Email == "" { return inbound, false, common.NewError("empty client ID") } - } else { + default: if client.ID == "" { return inbound, false, common.NewError("empty client ID") } @@ -436,15 +437,16 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) { // Secure client ID for _, client := range clients { - if oldInbound.Protocol == "trojan" { + switch oldInbound.Protocol { + case "trojan": if client.Password == "" { return false, common.NewError("empty client ID") } - } else if oldInbound.Protocol == "shadowsocks" { + case "shadowsocks": if client.Email == "" { return false, common.NewError("empty client ID") } - } else { + default: if client.ID == "" { return false, common.NewError("empty client ID") } @@ -631,13 +633,14 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin clientIndex := -1 for index, oldClient := range oldClients { oldClientId := "" - if oldInbound.Protocol == "trojan" { + switch oldInbound.Protocol { + case "trojan": oldClientId = oldClient.Password newClientId = clients[0].Password - } else if oldInbound.Protocol == "shadowsocks" { + case "shadowsocks": oldClientId = oldClient.Email newClientId = clients[0].Email - } else { + default: oldClientId = oldClient.ID newClientId = clients[0].ID } @@ -1244,11 +1247,12 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId int64) (boo for _, oldClient := range oldClients { if oldClient.Email == clientEmail { - if inbound.Protocol == "trojan" { + switch inbound.Protocol { + case "trojan": clientId = oldClient.Password - } else if inbound.Protocol == "shadowsocks" { + case "shadowsocks": clientId = oldClient.Email - } else { + default: clientId = oldClient.ID } break @@ -1328,11 +1332,12 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo for _, oldClient := range oldClients { if oldClient.Email == clientEmail { - if inbound.Protocol == "trojan" { + switch inbound.Protocol { + case "trojan": clientId = oldClient.Password - } else if inbound.Protocol == "shadowsocks" { + case "shadowsocks": clientId = oldClient.Email - } else { + default: clientId = oldClient.ID } clientOldEnabled = oldClient.Enable @@ -1391,11 +1396,12 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int for _, oldClient := range oldClients { if oldClient.Email == clientEmail { - if inbound.Protocol == "trojan" { + switch inbound.Protocol { + case "trojan": clientId = oldClient.Password - } else if inbound.Protocol == "shadowsocks" { + case "shadowsocks": clientId = oldClient.Email - } else { + default: clientId = oldClient.ID } break @@ -1448,11 +1454,12 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry for _, oldClient := range oldClients { if oldClient.Email == clientEmail { - if inbound.Protocol == "trojan" { + switch inbound.Protocol { + case "trojan": clientId = oldClient.Password - } else if inbound.Protocol == "shadowsocks" { + case "shadowsocks": clientId = oldClient.Email - } else { + default: clientId = oldClient.ID } break @@ -1508,11 +1515,12 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota for _, oldClient := range oldClients { if oldClient.Email == clientEmail { - if inbound.Protocol == "trojan" { + switch inbound.Protocol { + case "trojan": clientId = oldClient.Password - } else if inbound.Protocol == "shadowsocks" { + case "shadowsocks": clientId = oldClient.Email - } else { + default: clientId = oldClient.ID } break diff --git a/web/service/tgbot.go b/web/service/tgbot.go index 301f64fd..6fdb4add 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -40,7 +40,6 @@ var ( isRunning bool hostname string hashStorage *global.HashStorage - handler *th.Handler // clients data to adding new client receiver_inbound_ID int @@ -641,13 +640,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool if len(dataArray) == 4 { num, err := strconv.Atoi(dataArray[3]) if err == nil { - if num == -2 { + switch num { + case -2: inputNumber = 0 - } else if num == -1 { + case -1: if inputNumber > 0 { inputNumber = (inputNumber / 10) } - } else { + default: inputNumber = (inputNumber * 10) + num } } @@ -704,6 +704,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool return } message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) @@ -715,13 +719,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool if len(dataArray) == 3 { num, err := strconv.Atoi(dataArray[2]) if err == nil { - if num == -2 { + switch num { + case -2: inputNumber = 0 - } else if num == -1 { + case -1: if inputNumber > 0 { inputNumber = (inputNumber / 10) } - } else { + default: inputNumber = (inputNumber * 10) + num } } @@ -844,13 +849,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool if len(dataArray) == 4 { num, err := strconv.Atoi(dataArray[3]) if err == nil { - if num == -2 { + switch num { + case -2: inputNumber = 0 - } else if num == -1 { + case -1: if inputNumber > 0 { inputNumber = (inputNumber / 10) } - } else { + default: inputNumber = (inputNumber * 10) + num } } @@ -919,6 +925,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool return } message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) @@ -930,13 +940,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool if len(dataArray) == 3 { num, err := strconv.Atoi(dataArray[2]) if err == nil { - if num == -2 { + switch num { + case -2: inputNumber = 0 - } else if num == -1 { + case -1: if inputNumber > 0 { inputNumber = (inputNumber / 10) } - } else { + default: inputNumber = (inputNumber * 10) + num } } @@ -1035,13 +1046,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool if len(dataArray) == 4 { num, err := strconv.Atoi(dataArray[3]) if err == nil { - if num == -2 { + switch num { + case -2: inputNumber = 0 - } else if num == -1 { + case -1: if inputNumber > 0 { inputNumber = (inputNumber / 10) } - } else { + default: inputNumber = (inputNumber * 10) + num } } @@ -1101,6 +1113,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool return } message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) @@ -1112,13 +1128,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool if len(dataArray) == 3 { num, err := strconv.Atoi(dataArray[2]) if err == nil { - if num == -2 { + switch num { + case -2: inputNumber = 0 - } else if num == -1 { + case -1: if inputNumber > 0 { inputNumber = (inputNumber / 10) } - } else { + default: inputNumber = (inputNumber * 10) + num } } @@ -1288,6 +1305,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool } message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } t.addClient(callbackQuery.Message.GetChat().ID, message_text) } @@ -1524,6 +1545,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool return } message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } t.addClient(chatId, message_text, messageId) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email)) case "add_client_default_ip_limit": @@ -1534,6 +1559,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool return } message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } t.addClient(chatId, message_text, messageId) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email)) case "add_client_submit_disable": @@ -1598,6 +1627,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool return } valid_emails, extra_emails, err := t.inboundService.FilterAndSortClientEmails(emails) + if err != nil { + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation"), tu.ReplyKeyboardRemove()) + return + } for _, valid_emails := range valid_emails { traffic, err := t.inboundService.GetClientTrafficByEmail(valid_emails) @@ -1760,6 +1793,10 @@ func (t *Tgbot) SubmitAddClient() (bool, error) { } jsonString, err := t.BuildJSONForProtocol(inbound.Protocol) + if err != nil { + logger.Warning("BuildJSONForProtocol run failed:", err) + return false, errors.New("failed to build JSON for protocol") + } newInbound := &model.Inbound{ Id: receiver_inbound_ID, @@ -2008,10 +2045,11 @@ func (t *Tgbot) UserLoginNotify(username string, password string, ip string, tim } msg := "" - if status == LoginSuccess { + switch status { + case LoginSuccess: msg += t.I18nBot("tgbot.messages.loginSuccess") msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname) - } else if status == LoginFail { + case LoginFail: msg += t.I18nBot("tgbot.messages.loginFailed") msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname) msg += t.I18nBot("tgbot.messages.password", "Password=="+password) From 2198e7a28f51949cdf74cbd6087bf14a391cb7d7 Mon Sep 17 00:00:00 2001 From: Alireza Ahmand Date: Sun, 17 Aug 2025 15:13:25 +0330 Subject: [PATCH 07/27] feat: Add remaining time to tgbot #3355 (#3360) --- web/service/tgbot.go | 16 ++++++++++++++++ web/translation/translate.ar_EG.toml | 27 ++++++++++++++------------- web/translation/translate.en_US.toml | 1 + web/translation/translate.es_ES.toml | 23 ++++++++++++----------- web/translation/translate.fa_IR.toml | 23 ++++++++++++----------- web/translation/translate.id_ID.toml | 13 +++++++------ web/translation/translate.ja_JP.toml | 19 ++++++++++--------- web/translation/translate.pt_BR.toml | 9 +++++---- web/translation/translate.ru_RU.toml | 1 + web/translation/translate.tr_TR.toml | 13 +++++++------ web/translation/translate.uk_UA.toml | 13 +++++++------ web/translation/translate.vi_VN.toml | 21 +++++++++++---------- web/translation/translate.zh_CN.toml | 11 ++++++----- web/translation/translate.zh_TW.toml | 15 ++++++++------- 14 files changed, 117 insertions(+), 88 deletions(-) diff --git a/web/service/tgbot.go b/web/service/tgbot.go index 6fdb4add..e957ff9f 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -2209,6 +2209,22 @@ func (t *Tgbot) clientInfoMsg( expiryTime = t.I18nBot("tgbot.unlimited") } else if diff > 172800 || !traffic.Enable { expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") + if diff > 0 { + days := diff / 86400 + hours := (diff % 86400) / 3600 + minutes := (diff % 3600) / 60 + remainingTime := "" + if days > 0 { + remainingTime += fmt.Sprintf("%d %s ", days, t.I18nBot("tgbot.days")) + } + if hours > 0 { + remainingTime += fmt.Sprintf("%d %s ", hours, t.I18nBot("tgbot.hours")) + } + if minutes > 0 { + remainingTime += fmt.Sprintf("%d %s", minutes, t.I18nBot("tgbot.minutes")) + } + expiryTime += fmt.Sprintf(" (%s)", remainingTime) + } } else if traffic.ExpiryTime < 0 { expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days")) flag = true diff --git a/web/translation/translate.ar_EG.toml b/web/translation/translate.ar_EG.toml index 19d451fa..f1c1919e 100644 --- a/web/translation/translate.ar_EG.toml +++ b/web/translation/translate.ar_EG.toml @@ -561,24 +561,25 @@ "resetOutboundTrafficError" = "خطأ في إعادة تعيين حركات المرور الصادرة" [tgbot] -"keyboardClosed" = "❌ الكيبورد المخصص اتقفلت!" -"noResult" = "❗ مفيش نتيجة!" -"noQuery" = "❌ مش لاقي السؤال! استخدم الأمر تاني!" -"wentWrong" = "❌ حصل خطأ!" -"noIpRecord" = "❗ مفيش سجل IP!" -"noInbounds" = "❗ مفيش إدخال متواجد!" -"unlimited" = "♾ غير محدود (إعادة ضبط)" -"add" = "أضف" +"keyboardClosed" = "❌ لوحة المفاتيح مغلقة!" +"noResult" = "❗ لا يوجد نتائج!" +"noQuery" = "❌ لم يتم العثور على الاستعلام! يرجى استخدام الأمر مرة أخرى!" +"wentWrong" = "❌ حدث خطأ ما!" +"noIpRecord" = "❗ لا يوجد سجل IP!" +"noInbounds" = "❗ لم يتم العثور على أي وارد!" +"unlimited" = "♾ غير محدود (إعادة تعيين)" +"add" = "إضافة" "month" = "شهر" -"months" = "شهور" +"months" = "أشهر" "day" = "يوم" "days" = "أيام" "hours" = "ساعات" -"unknown" = "مش معروف" -"inbounds" = "الإدخالات" +"minutes" = "دقائق" +"unknown" = "غير معروف" +"inbounds" = "الواردات" "clients" = "العملاء" -"offline" = "🔴 أوفلاين" -"online" = "🟢 أونلاين" +"offline" = "🔴 غير متصل" +"online" = "🟢 متصل" [tgbot.commands] "unknown" = "❗ أمر مش معروف." diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index e0ce49aa..abff6bc7 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -574,6 +574,7 @@ "day" = "Day" "days" = "Days" "hours" = "Hours" +"minutes" = "Minutes" "unknown" = "Unknown" "inbounds" = "Inbounds" "clients" = "Clients" diff --git a/web/translation/translate.es_ES.toml b/web/translation/translate.es_ES.toml index 73cf110b..883f4e89 100644 --- a/web/translation/translate.es_ES.toml +++ b/web/translation/translate.es_ES.toml @@ -91,7 +91,7 @@ "invalidFormData" = "El formato de los datos de entrada es inválido." "emptyUsername" = "Por favor ingresa el nombre de usuario." "emptyPassword" = "Por favor ingresa la contraseña." -"wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto." +"wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto." "successLogin" = "Has iniciado sesión en tu cuenta correctamente." [pages.index] @@ -535,9 +535,9 @@ [pages.settings.security] "admin" = "Credenciales de administrador" -"twoFactor" = "Autenticación de dos factores" -"twoFactorEnable" = "Habilitar 2FA" -"twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad." +"twoFactor" = "Autenticación de dos factores" +"twoFactorEnable" = "Habilitar 2FA" +"twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad." "twoFactorModalSetTitle" = "Activar autenticación de dos factores" "twoFactorModalDeleteTitle" = "Desactivar autenticación de dos factores" "twoFactorModalSteps" = "Para configurar la autenticación de dos factores, sigue estos pasos:" @@ -561,23 +561,24 @@ "resetOutboundTrafficError" = "Error al reiniciar el tráfico saliente" [tgbot] -"keyboardClosed" = "❌ ¡Teclado personalizado cerrado!" -"noResult" = "❗ ¡Sin resultados!" -"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor utiliza el comando nuevamente!" +"keyboardClosed" = "❌ Teclado cerrado!" +"noResult" = "❗ ¡No hay resultados!" +"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor, use el comando de nuevo!" "wentWrong" = "❌ ¡Algo salió mal!" -"noIpRecord" = "❗ ¡Sin Registro de IP!" +"noIpRecord" = "❗ ¡No hay registro de IP!" "noInbounds" = "❗ ¡No se encontraron entradas!" -"unlimited" = "♾ Ilimitado" -"add" = "Agregar" +"unlimited" = "♾ Ilimitado (Restablecer)" +"add" = "Añadir" "month" = "Mes" "months" = "Meses" "day" = "Día" "days" = "Días" "hours" = "Horas" +"minutes" = "Minutos" "unknown" = "Desconocido" "inbounds" = "Entradas" "clients" = "Clientes" -"offline" = "🔴 Sin conexión" +"offline" = "🔴 Desconectado" "online" = "🟢 En línea" [tgbot.commands] diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml index 8e2b04a2..787a695a 100644 --- a/web/translation/translate.fa_IR.toml +++ b/web/translation/translate.fa_IR.toml @@ -561,22 +561,23 @@ "resetOutboundTrafficError" = "خطا در بازنشانی ترافیک خروجی" [tgbot] -"keyboardClosed" = "❌ کیبورد سفارشی بسته شد!" -"noResult" = "❗ نتیجه‌ای یافت نشد!" -"noQuery" = "❌ کوئری یافت نشد! لطفاً دستور را مجدداً استفاده کنید!" -"wentWrong" = "❌ مشکلی رخ داده است!" -"noIpRecord" = "❗ رکورد IP یافت نشد!" +"keyboardClosed" = "❌ صفحه کلید بسته شد!" +"noResult" = "❗ نتیجه ای یافت نشد!" +"noQuery" = "❌ درخواست یافت نشد! لطفا دوباره تلاش کنید!" +"wentWrong" = "❌ مشکلی پیش آمد!" +"noIpRecord" = "❗ رکورد آی پی وجود ندارد!" "noInbounds" = "❗ هیچ ورودی یافت نشد!" -"unlimited" = "♾ - نامحدود(ریست)" -"add" = "اضافه کردن" +"unlimited" = "♾ نامحدود(ریست)" +"add" = "افزودن" "month" = "ماه" -"months" = "ماه‌" +"months" = "ماه" "day" = "روز" "days" = "روز" -"hours" = "ساعت‌" +"hours" = "ساعت" +"minutes" = "دقیقه" "unknown" = "نامشخص" -"inbounds" = "ورودی‌ها" -"clients" = "کلاینت‌ها" +"inbounds" = "ورودی ها" +"clients" = "کاربران" "offline" = "🔴 آفلاین" "online" = "🟢 آنلاین" diff --git a/web/translation/translate.id_ID.toml b/web/translation/translate.id_ID.toml index bb30ead9..5f4c648d 100644 --- a/web/translation/translate.id_ID.toml +++ b/web/translation/translate.id_ID.toml @@ -561,21 +561,22 @@ "resetOutboundTrafficError" = "Gagal mereset lalu lintas keluar" [tgbot] -"keyboardClosed" = "❌ Papan ketik kustom ditutup!" +"keyboardClosed" = "❌ Keyboard ditutup!" "noResult" = "❗ Tidak ada hasil!" -"noQuery" = "❌ Permintaan tidak ditemukan! Harap gunakan perintah lagi!" -"wentWrong" = "❌ Ada yang salah!" +"noQuery" = "❌ Kueri tidak ditemukan! Silakan gunakan perintah lagi!" +"wentWrong" = "❌ Terjadi kesalahan!" "noIpRecord" = "❗ Tidak ada Catatan IP!" -"noInbounds" = "❗ Tidak ada masuk ditemukan!" -"unlimited" = "♾ Tak terbatas" +"noInbounds" = "❗ Tidak ada inbound yang ditemukan!" +"unlimited" = "♾ Tidak terbatas (Reset)" "add" = "Tambah" "month" = "Bulan" "months" = "Bulan" "day" = "Hari" "days" = "Hari" "hours" = "Jam" +"minutes" = "Menit" "unknown" = "Tidak diketahui" -"inbounds" = "Masuk" +"inbounds" = "Inbound" "clients" = "Klien" "offline" = "🔴 Offline" "online" = "🟢 Online" diff --git a/web/translation/translate.ja_JP.toml b/web/translation/translate.ja_JP.toml index eed4cc94..8efa9074 100644 --- a/web/translation/translate.ja_JP.toml +++ b/web/translation/translate.ja_JP.toml @@ -561,21 +561,22 @@ "resetOutboundTrafficError" = "送信トラフィックのリセットエラー" [tgbot] -"keyboardClosed" = "❌ カスタムキーボードが閉じられました!" +"keyboardClosed" = "❌ キーボードを閉じました!" "noResult" = "❗ 結果がありません!" -"noQuery" = "❌ クエリが見つかりませんでした!もう一度コマンドを使用してください!" -"wentWrong" = "❌ 問題が発生しました!" -"noIpRecord" = "❗ IP記録がありません!" -"noInbounds" = "❗ インバウンド接続が見つかりません!" -"unlimited" = "♾ 無制限" +"noQuery" = "❌ クエリが見つかりません!コマンドを再利用してください!" +"wentWrong" = "❌ 何かがうまくいかなかった!" +"noIpRecord" = "❗ IPレコードがありません!" +"noInbounds" = "❗ インバウンドが見つかりません!" +"unlimited" = "♾ 無制限(リセット)" "add" = "追加" "month" = "月" -"months" = "月" +"months" = "ヶ月" "day" = "日" -"days" = "日" +"days" = "日間" "hours" = "時間" +"minutes" = "分" "unknown" = "不明" -"inbounds" = "インバウンド接続" +"inbounds" = "インバウンド" "clients" = "クライアント" "offline" = "🔴 オフライン" "online" = "🟢 オンライン" diff --git a/web/translation/translate.pt_BR.toml b/web/translation/translate.pt_BR.toml index 6f12077d..0d012591 100644 --- a/web/translation/translate.pt_BR.toml +++ b/web/translation/translate.pt_BR.toml @@ -561,21 +561,22 @@ "resetOutboundTrafficError" = "Erro ao redefinir tráfego de saída" [tgbot] -"keyboardClosed" = "❌ Teclado personalizado fechado!" +"keyboardClosed" = "❌ Teclado fechado!" "noResult" = "❗ Nenhum resultado!" "noQuery" = "❌ Consulta não encontrada! Por favor, use o comando novamente!" "wentWrong" = "❌ Algo deu errado!" "noIpRecord" = "❗ Nenhum registro de IP!" -"noInbounds" = "❗ Nenhuma entrada encontrada!" -"unlimited" = "♾ Ilimitado (Reiniciar)" +"noInbounds" = "❗ Nenhum inbound encontrado!" +"unlimited" = "♾ Ilimitado (Reset)" "add" = "Adicionar" "month" = "Mês" "months" = "Meses" "day" = "Dia" "days" = "Dias" "hours" = "Horas" +"minutes" = "Minutos" "unknown" = "Desconhecido" -"inbounds" = "Entradas" +"inbounds" = "Inbounds" "clients" = "Clientes" "offline" = "🔴 Offline" "online" = "🟢 Online" diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index f9ea047e..558d309b 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -574,6 +574,7 @@ "day" = "День" "days" = "Дней" "hours" = "Часов" +"minutes" = "Минуты" "unknown" = "Неизвестно" "inbounds" = "Инбаунды" "clients" = "Клиенты" diff --git a/web/translation/translate.tr_TR.toml b/web/translation/translate.tr_TR.toml index 803becd8..32dca2ea 100644 --- a/web/translation/translate.tr_TR.toml +++ b/web/translation/translate.tr_TR.toml @@ -561,22 +561,23 @@ "resetOutboundTrafficError" = "Giden trafik sıfırlanırken hata" [tgbot] -"keyboardClosed" = "❌ Özel klavye kapalı!" +"keyboardClosed" = "❌ Klavye kapatıldı!" "noResult" = "❗ Sonuç yok!" "noQuery" = "❌ Sorgu bulunamadı! Lütfen komutu tekrar kullanın!" "wentWrong" = "❌ Bir şeyler yanlış gitti!" -"noIpRecord" = "❗ IP Kaydı yok!" -"noInbounds" = "❗ Gelen bulunamadı!" -"unlimited" = "♾ Sınırsız(Sıfırla)" +"noIpRecord" = "❗ IP Kaydı Yok!" +"noInbounds" = "❗ Gelen bağlantı bulunamadı!" +"unlimited" = "♾ Sınırsız (Sıfırla)" "add" = "Ekle" "month" = "Ay" "months" = "Aylar" "day" = "Gün" "days" = "Günler" "hours" = "Saatler" -"unknown" = "Bilinmiyor" +"minutes" = "Dakika" +"unknown" = "Bilinmeyen" "inbounds" = "Gelenler" -"clients" = "Müşteriler" +"clients" = "İstemciler" "offline" = "🔴 Çevrimdışı" "online" = "🟢 Çevrimiçi" diff --git a/web/translation/translate.uk_UA.toml b/web/translation/translate.uk_UA.toml index d181744e..8976b66a 100644 --- a/web/translation/translate.uk_UA.toml +++ b/web/translation/translate.uk_UA.toml @@ -561,19 +561,20 @@ "resetOutboundTrafficError" = "Помилка скидання вихідного трафіку" [tgbot] -"keyboardClosed" = "❌ Спеціальна клавіатура закрита!" +"keyboardClosed" = "❌ Клавіатуру закрито!" "noResult" = "❗ Немає результату!" -"noQuery" = "❌ Запит не знайдено! Скористайтеся командою ще раз!" +"noQuery" = "❌ Запит не знайдено! Будь ласка, використовуйте команду ще раз!" "wentWrong" = "❌ Щось пішло не так!" -"noIpRecord" = "❗ Немає IP-запису!" -"noInbounds" = "❗ Вхідних не знайдено!" -"unlimited" = "♾ Необмежений (скинути)" +"noIpRecord" = "❗ Немає запису IP!" +"noInbounds" = "❗ Вхідні не знайдені!" +"unlimited" = "♾ Необмежено (Скинути)" "add" = "Додати" "month" = "Місяць" "months" = "Місяці" "day" = "День" "days" = "Дні" -"hours" = "Годинник" +"hours" = "Години" +"minutes" = "Хвилини" "unknown" = "Невідомо" "inbounds" = "Вхідні" "clients" = "Клієнти" diff --git a/web/translation/translate.vi_VN.toml b/web/translation/translate.vi_VN.toml index 0283ae00..6e28a0de 100644 --- a/web/translation/translate.vi_VN.toml +++ b/web/translation/translate.vi_VN.toml @@ -91,7 +91,7 @@ "invalidFormData" = "Dạng dữ liệu nhập không hợp lệ." "emptyUsername" = "Vui lòng nhập tên người dùng." "emptyPassword" = "Vui lòng nhập mật khẩu." -"wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ." +"wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ." "successLogin" = "Bạn đã đăng nhập vào tài khoản thành công." [pages.index] @@ -535,9 +535,9 @@ [pages.settings.security] "admin" = "Thông tin đăng nhập quản trị viên" -"twoFactor" = "Xác thực hai yếu tố" -"twoFactorEnable" = "Bật 2FA" -"twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn." +"twoFactor" = "Xác thực hai yếu tố" +"twoFactorEnable" = "Bật 2FA" +"twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn." "twoFactorModalSetTitle" = "Bật xác thực hai yếu tố" "twoFactorModalDeleteTitle" = "Tắt xác thực hai yếu tố" "twoFactorModalSteps" = "Để thiết lập xác thực hai yếu tố, hãy thực hiện các bước sau:" @@ -561,22 +561,23 @@ "resetOutboundTrafficError" = "Lỗi khi đặt lại lưu lượng truy cập đi" [tgbot] -"keyboardClosed" = "❌ Bàn phím tùy chỉnh đã đóng!" +"keyboardClosed" = "❌ Bàn phím đã đóng!" "noResult" = "❗ Không có kết quả!" -"noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lệnh lại!" +"noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lại lệnh!" "wentWrong" = "❌ Đã xảy ra lỗi!" "noIpRecord" = "❗ Không có bản ghi IP!" "noInbounds" = "❗ Không tìm thấy inbound!" -"unlimited" = "♾ Không giới hạn" +"unlimited" = "♾ Không giới hạn (Đặt lại)" "add" = "Thêm" "month" = "Tháng" "months" = "Tháng" "day" = "Ngày" "days" = "Ngày" "hours" = "Giờ" -"unknown" = "Không rõ" -"inbounds" = "Vào" -"clients" = "Các người dùng" +"minutes" = "Phút" +"unknown" = "Không xác định" +"inbounds" = "Inbound" +"clients" = "Client" "offline" = "🔴 Ngoại tuyến" "online" = "🟢 Trực tuyến" diff --git a/web/translation/translate.zh_CN.toml b/web/translation/translate.zh_CN.toml index ba93e980..a2142bd1 100644 --- a/web/translation/translate.zh_CN.toml +++ b/web/translation/translate.zh_CN.toml @@ -563,19 +563,20 @@ [tgbot] "keyboardClosed" = "❌ 自定义键盘已关闭!" "noResult" = "❗ 没有结果!" -"noQuery" = "❌ 未找到查询!请重新使用命令!" +"noQuery" = "❌ 未找到查询!请再次使用该命令!" "wentWrong" = "❌ 出了点问题!" -"noIpRecord" = "❗ 没有 IP 记录!" -"noInbounds" = "❗ 没有找到入站连接!" -"unlimited" = "♾ 无限制" +"noIpRecord" = "❗ 没有IP记录!" +"noInbounds" = "❗ 未找到入站!" +"unlimited" = "♾ 无限(重置)" "add" = "添加" "month" = "月" "months" = "月" "day" = "天" "days" = "天" "hours" = "小时" +"minutes" = "分钟" "unknown" = "未知" -"inbounds" = "入站连接" +"inbounds" = "入站" "clients" = "客户端" "offline" = "🔴 离线" "online" = "🟢 在线" diff --git a/web/translation/translate.zh_TW.toml b/web/translation/translate.zh_TW.toml index eef39ab6..db5b33ed 100644 --- a/web/translation/translate.zh_TW.toml +++ b/web/translation/translate.zh_TW.toml @@ -563,22 +563,23 @@ [tgbot] "keyboardClosed" = "❌ 自定義鍵盤已關閉!" "noResult" = "❗ 沒有結果!" -"noQuery" = "❌ 未找到查詢!請重新使用命令!" +"noQuery" = "❌ 未找到查詢!請再次使用該命令!" "wentWrong" = "❌ 出了點問題!" -"noIpRecord" = "❗ 沒有 IP 記錄!" -"noInbounds" = "❗ 沒有找到入站連線!" -"unlimited" = "♾ 無限制" -"add" = "新增" +"noIpRecord" = "❗ 沒有IP記錄!" +"noInbounds" = "❗ 未找到入站!" +"unlimited" = "♾ 無限(重置)" +"add" = "添加" "month" = "月" "months" = "月" "day" = "天" "days" = "天" "hours" = "小時" +"minutes" = "分鐘" "unknown" = "未知" -"inbounds" = "入站連線" +"inbounds" = "入站" "clients" = "客戶端" "offline" = "🔴 離線" -"online" = "🟢 線上" +"online" = "🟢 在線" [tgbot.commands] "unknown" = "❗ 未知命令" From 24a341146568446a0a787f7955581fa3531c40d5 Mon Sep 17 00:00:00 2001 From: mhsanaei Date: Thu, 21 Aug 2025 14:24:25 +0200 Subject: [PATCH 08/27] more list for public IP address --- install.sh | 17 ++++++++++++----- web/service/server.go | 17 +++++++++++++++-- x-ui.sh | 19 +++++++++++++++---- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/install.sh b/install.sh index ce59abe6..4c959a2a 100644 --- a/install.sh +++ b/install.sh @@ -7,7 +7,6 @@ yellow='\033[0;33m' plain='\033[0m' cur_dir=$(pwd) -show_ip_service_lists=("https://api.ipify.org" "https://4.ident.me") # check root [[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1 @@ -73,10 +72,18 @@ config_after_install() { local existing_hasDefaultCredential=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}') local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') - - for ip_service_addr in "${show_ip_service_lists[@]}"; do - local server_ip=$(curl -s --max-time 3 ${ip_service_addr} 2>/dev/null) - if [ -n "${server_ip}" ]; then + local URL_lists=( + "https://api4.ipify.org" + "https://ipv4.icanhazip.com" + "https://v4.api.ipinfo.io/ip" + "https://ipv4.myexternalip.com/raw" + "https://4.ident.me" + "https://check-host.net/ip" + ) + local server_ip="" + for ip_address in "${URL_lists[@]}"; do + server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]') + if [[ -n "${server_ip}" ]]; then break fi done diff --git a/web/service/server.go b/web/service/server.go index 6b4e5d63..2dc83d77 100644 --- a/web/service/server.go +++ b/web/service/server.go @@ -235,8 +235,21 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status { } // IP fetching with caching - showIp4ServiceLists := []string{"https://api.ipify.org", "https://4.ident.me"} - showIp6ServiceLists := []string{"https://api6.ipify.org", "https://6.ident.me"} + showIp4ServiceLists := []string{ + "https://api4.ipify.org", + "https://ipv4.icanhazip.com", + "https://v4.api.ipinfo.io/ip", + "https://ipv4.myexternalip.com/raw", + "https://4.ident.me", + "https://check-host.net/ip", + } + showIp6ServiceLists := []string{ + "https://api6.ipify.org", + "https://ipv6.icanhazip.com", + "https://v6.api.ipinfo.io/ip", + "https://ipv6.myexternalip.com/raw", + "https://6.ident.me", + } if s.cachedIPv4 == "" { for _, ip4Service := range showIp4ServiceLists { diff --git a/x-ui.sh b/x-ui.sh index 4acf2dd6..cec86ba0 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -1643,10 +1643,21 @@ iplimit_remove_conflicts() { } SSH_port_forwarding() { - local server_ip=$(curl -s --max-time 3 https://api.ipify.org) - if [ -z "$server_ip" ]; then - server_ip=$(curl -s --max-time 3 https://4.ident.me) - fi + local URL_lists=( + "https://api4.ipify.org" + "https://ipv4.icanhazip.com" + "https://v4.api.ipinfo.io/ip" + "https://ipv4.myexternalip.com/raw" + "https://4.ident.me" + "https://check-host.net/ip" + ) + local server_ip="" + for ip_address in "${URL_lists[@]}"; do + server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]') + if [[ -n "${server_ip}" ]]; then + break + fi + done local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') local existing_listenIP=$(/usr/local/x-ui/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}') From d10c312e62e0abf6da64e21a55c51151e23d9929 Mon Sep 17 00:00:00 2001 From: Igor Finagin Date: Mon, 25 Aug 2025 15:42:02 +0400 Subject: [PATCH 09/27] AutoFill OTP (#3381) https://developer.apple.com/documentation/security/enabling-password-autofill-on-an-html-input-element --- web/html/login.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/html/login.html b/web/html/login.html index 69a8400f..b6dfc299 100644 --- a/web/html/login.html +++ b/web/html/login.html @@ -512,7 +512,7 @@ - @@ -615,4 +615,4 @@ } }); -{{ template "page/body_end" .}} \ No newline at end of file +{{ template "page/body_end" .}} From 21983971971b14377b36c8db92c8603f723f955d Mon Sep 17 00:00:00 2001 From: Ali Golzar <57574919+aliglzr@users.noreply.github.com> Date: Wed, 27 Aug 2025 21:00:49 +0330 Subject: [PATCH 10/27] Created / Updated fields for clients (#3384) * feat(backend): add created_at/updated_at to clients and maintain on create/update backfill existing clients and set updated_at on mutations * feat(frontend): carry created_at/updated_at in client models and round-trip via JSON * feat(frontend): display Created and Updated columns in client table with proper date formatting * i18n: add pages.inbounds.createdAt/updatedAt across all locales * Update inbound.go Remove duplicate code --- database/model/model.go | 2 + web/assets/js/model/inbound.js | 36 ++++++++- web/html/component/aClientTable.html | 26 ++++++ web/html/inbounds.html | 2 + web/service/inbound.go | 117 +++++++++++++++++++++++++++ web/translation/translate.ar_EG.toml | 2 + web/translation/translate.en_US.toml | 2 + web/translation/translate.es_ES.toml | 2 + web/translation/translate.fa_IR.toml | 2 + web/translation/translate.id_ID.toml | 2 + web/translation/translate.ja_JP.toml | 2 + web/translation/translate.pt_BR.toml | 2 + web/translation/translate.ru_RU.toml | 2 + web/translation/translate.tr_TR.toml | 2 + web/translation/translate.uk_UA.toml | 2 + web/translation/translate.vi_VN.toml | 2 + web/translation/translate.zh_CN.toml | 2 + web/translation/translate.zh_TW.toml | 2 + 18 files changed, 205 insertions(+), 4 deletions(-) diff --git a/database/model/model.go b/database/model/model.go index 2e7095d3..86ab0487 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -104,4 +104,6 @@ type Client struct { SubID string `json:"subId" form:"subId"` Comment string `json:"comment" form:"comment"` Reset int `json:"reset" form:"reset"` + CreatedAt int64 `json:"created_at,omitempty"` + UpdatedAt int64 `json:"updated_at,omitempty"` } diff --git a/web/assets/js/model/inbound.js b/web/assets/js/model/inbound.js index 803b5d94..33aa24e0 100644 --- a/web/assets/js/model/inbound.js +++ b/web/assets/js/model/inbound.js @@ -1817,7 +1817,9 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass { tgId = '', subId = RandomUtil.randomLowerAndNum(16), comment = '', - reset = 0 + reset = 0, + created_at = undefined, + updated_at = undefined ) { super(); this.id = id; @@ -1831,6 +1833,8 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass { this.subId = subId; this.comment = comment; this.reset = reset; + this.created_at = created_at; + this.updated_at = updated_at; } static fromJson(json = {}) { @@ -1846,6 +1850,8 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass { json.subId, json.comment, json.reset, + json.created_at, + json.updated_at, ); } get _expiryTime() { @@ -1926,7 +1932,9 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass { tgId = '', subId = RandomUtil.randomLowerAndNum(16), comment = '', - reset = 0 + reset = 0, + created_at = undefined, + updated_at = undefined ) { super(); this.id = id; @@ -1940,6 +1948,8 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass { this.subId = subId; this.comment = comment; this.reset = reset; + this.created_at = created_at; + this.updated_at = updated_at; } static fromJson(json = {}) { @@ -1955,6 +1965,8 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass { json.subId, json.comment, json.reset, + json.created_at, + json.updated_at, ); } @@ -2065,7 +2077,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass { tgId = '', subId = RandomUtil.randomLowerAndNum(16), comment = '', - reset = 0 + reset = 0, + created_at = undefined, + updated_at = undefined ) { super(); this.password = password; @@ -2078,6 +2092,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass { this.subId = subId; this.comment = comment; this.reset = reset; + this.created_at = created_at; + this.updated_at = updated_at; } toJson() { @@ -2092,6 +2108,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass { subId: this.subId, comment: this.comment, reset: this.reset, + created_at: this.created_at, + updated_at: this.updated_at, }; } @@ -2107,6 +2125,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass { json.subId, json.comment, json.reset, + json.created_at, + json.updated_at, ); } @@ -2226,7 +2246,9 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass { tgId = '', subId = RandomUtil.randomLowerAndNum(16), comment = '', - reset = 0 + reset = 0, + created_at = undefined, + updated_at = undefined ) { super(); this.method = method; @@ -2240,6 +2262,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass { this.subId = subId; this.comment = comment; this.reset = reset; + this.created_at = created_at; + this.updated_at = updated_at; } toJson() { @@ -2255,6 +2279,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass { subId: this.subId, comment: this.comment, reset: this.reset, + created_at: this.created_at, + updated_at: this.updated_at, }; } @@ -2271,6 +2297,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass { json.subId, json.comment, json.reset, + json.created_at, + json.updated_at, ); } diff --git a/web/html/component/aClientTable.html b/web/html/component/aClientTable.html index 96bd502f..359e6e74 100644 --- a/web/html/component/aClientTable.html +++ b/web/html/component/aClientTable.html @@ -278,4 +278,30 @@ + + {{end}} diff --git a/web/html/inbounds.html b/web/html/inbounds.html index 58d2d07a..010296eb 100644 --- a/web/html/inbounds.html +++ b/web/html/inbounds.html @@ -760,6 +760,8 @@ { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } }, { title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } }, { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } }, + { title: '{{ i18n "pages.inbounds.createdAt" }}', width: 90, align: 'center', scopedSlots: { customRender: 'createdAt' } }, + { title: '{{ i18n "pages.inbounds.updatedAt" }}', width: 90, align: 'center', scopedSlots: { customRender: 'updatedAt' } }, ]; const innerMobileColumns = [ diff --git a/web/service/inbound.go b/web/service/inbound.go index 6e10e798..4ef5fce3 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -175,6 +175,30 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo return inbound, false, err } + // Ensure created_at and updated_at on clients in settings + if len(clients) > 0 { + var settings map[string]any + if err2 := json.Unmarshal([]byte(inbound.Settings), &settings); err2 == nil && settings != nil { + now := time.Now().Unix() * 1000 + updatedClients := make([]model.Client, 0, len(clients)) + for _, c := range clients { + if c.CreatedAt == 0 { + c.CreatedAt = now + } + c.UpdatedAt = now + updatedClients = append(updatedClients, c) + } + settings["clients"] = updatedClients + if bs, err3 := json.MarshalIndent(settings, "", " "); err3 == nil { + inbound.Settings = string(bs) + } else { + logger.Debug("Unable to marshal inbound settings with timestamps:", err3) + } + } else if err2 != nil { + logger.Debug("Unable to parse inbound settings for timestamps:", err2) + } + } + // Secure client ID for _, client := range clients { switch inbound.Protocol { @@ -320,6 +344,53 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, return inbound, false, err } + // Ensure created_at and updated_at exist in inbound.Settings clients + { + var oldSettings map[string]any + _ = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings) + emailToCreated := map[string]int64{} + if oldSettings != nil { + if oc, ok := oldSettings["clients"].([]any); ok { + for _, it := range oc { + if m, ok2 := it.(map[string]any); ok2 { + if email, ok3 := m["email"].(string); ok3 { + switch v := m["created_at"].(type) { + case float64: + emailToCreated[email] = int64(v) + case int64: + emailToCreated[email] = v + } + } + } + } + } + } + var newSettings map[string]any + if err2 := json.Unmarshal([]byte(inbound.Settings), &newSettings); err2 == nil && newSettings != nil { + now := time.Now().Unix() * 1000 + if nSlice, ok := newSettings["clients"].([]any); ok { + for i := range nSlice { + if m, ok2 := nSlice[i].(map[string]any); ok2 { + email, _ := m["email"].(string) + if _, ok3 := m["created_at"]; !ok3 { + if v, ok4 := emailToCreated[email]; ok4 && v > 0 { + m["created_at"] = v + } else { + m["created_at"] = now + } + } + m["updated_at"] = now + nSlice[i] = m + } + } + newSettings["clients"] = nSlice + if bs, err3 := json.MarshalIndent(newSettings, "", " "); err3 == nil { + inbound.Settings = string(bs) + } + } + } + } + oldInbound.Up = inbound.Up oldInbound.Down = inbound.Down oldInbound.Total = inbound.Total @@ -422,6 +493,17 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) { } interfaceClients := settings["clients"].([]any) + // Add timestamps for new clients being appended + nowTs := time.Now().Unix() * 1000 + for i := range interfaceClients { + if cm, ok := interfaceClients[i].(map[string]any); ok { + if _, ok2 := cm["created_at"]; !ok2 { + cm["created_at"] = nowTs + } + cm["updated_at"] = nowTs + interfaceClients[i] = cm + } + } existEmail, err := s.checkEmailsExistForClients(clients) if err != nil { return false, err @@ -672,6 +754,25 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin return false, err } settingsClients := oldSettings["clients"].([]any) + // Preserve created_at and set updated_at for the replacing client + var preservedCreated any + if clientIndex >= 0 && clientIndex < len(settingsClients) { + if oldMap, ok := settingsClients[clientIndex].(map[string]any); ok { + if v, ok2 := oldMap["created_at"]; ok2 { + preservedCreated = v + } + } + } + if len(interfaceClients) > 0 { + if newMap, ok := interfaceClients[0].(map[string]any); ok { + if preservedCreated == nil { + preservedCreated = time.Now().Unix() * 1000 + } + newMap["created_at"] = preservedCreated + newMap["updated_at"] = time.Now().Unix() * 1000 + interfaceClients[0] = newMap + } + } settingsClients[clientIndex] = interfaceClients[0] oldSettings["clients"] = settingsClients @@ -909,10 +1010,16 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl oldExpiryTime := c["expiryTime"].(float64) newExpiryTime := (time.Now().Unix() * 1000) - int64(oldExpiryTime) c["expiryTime"] = newExpiryTime + c["updated_at"] = time.Now().Unix() * 1000 dbClientTraffics[traffic_index].ExpiryTime = newExpiryTime break } } + // Backfill created_at and updated_at + if _, ok := c["created_at"]; !ok { + c["created_at"] = time.Now().Unix() * 1000 + } + c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } settings["clients"] = newClients @@ -1274,6 +1381,7 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId int64) (boo c := clients[client_index].(map[string]any) if c["email"] == clientEmail { c["tgId"] = tgId + c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } } @@ -1360,6 +1468,7 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo c := clients[client_index].(map[string]any) if c["email"] == clientEmail { c["enable"] = !clientOldEnabled + c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } } @@ -1423,6 +1532,7 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int c := clients[client_index].(map[string]any) if c["email"] == clientEmail { c["limitIp"] = count + c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } } @@ -1481,6 +1591,7 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry c := clients[client_index].(map[string]any) if c["email"] == clientEmail { c["expiryTime"] = expiry_time + c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } } @@ -1542,6 +1653,7 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota c := clients[client_index].(map[string]any) if c["email"] == clientEmail { c["totalGB"] = totalGB * 1024 * 1024 * 1024 + c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } } @@ -1962,6 +2074,11 @@ func (s *InboundService) MigrationRequirements() { c["flow"] = "" } } + // Backfill created_at and updated_at + if _, ok := c["created_at"]; !ok { + c["created_at"] = time.Now().Unix() * 1000 + } + c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } settings["clients"] = newClients diff --git a/web/translation/translate.ar_EG.toml b/web/translation/translate.ar_EG.toml index f1c1919e..4e4aac75 100644 --- a/web/translation/translate.ar_EG.toml +++ b/web/translation/translate.ar_EG.toml @@ -165,6 +165,8 @@ "details" = "تفاصيل" "transportConfig" = "نقل" "expireDate" = "المدة" +"createdAt" = "تاريخ الإنشاء" +"updatedAt" = "تاريخ التحديث" "resetTraffic" = "إعادة ضبط الترافيك" "addInbound" = "أضف إدخال" "generalActions" = "إجراءات عامة" diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index abff6bc7..4e27908d 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -165,6 +165,8 @@ "details" = "Details" "transportConfig" = "Transport" "expireDate" = "Duration" +"createdAt" = "Created" +"updatedAt" = "Updated" "resetTraffic" = "Reset Traffic" "addInbound" = "Add Inbound" "generalActions" = "General Actions" diff --git a/web/translation/translate.es_ES.toml b/web/translation/translate.es_ES.toml index 883f4e89..3ad93de2 100644 --- a/web/translation/translate.es_ES.toml +++ b/web/translation/translate.es_ES.toml @@ -165,6 +165,8 @@ "details" = "Detalles" "transportConfig" = "Transporte" "expireDate" = "Fecha de Expiración" +"createdAt" = "Creado" +"updatedAt" = "Actualizado" "resetTraffic" = "Restablecer Tráfico" "addInbound" = "Agregar Entrada" "generalActions" = "Acciones Generales" diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml index 787a695a..5e810e62 100644 --- a/web/translation/translate.fa_IR.toml +++ b/web/translation/translate.fa_IR.toml @@ -165,6 +165,8 @@ "details" = "توضیحات" "transportConfig" = "نحوه اتصال" "expireDate" = "مدت زمان" +"createdAt" = "ایجاد" +"updatedAt" = "به‌روزرسانی" "resetTraffic" = "ریست ترافیک" "addInbound" = "افزودن ورودی" "generalActions" = "عملیات کلی" diff --git a/web/translation/translate.id_ID.toml b/web/translation/translate.id_ID.toml index 5f4c648d..dcfe68c0 100644 --- a/web/translation/translate.id_ID.toml +++ b/web/translation/translate.id_ID.toml @@ -165,6 +165,8 @@ "details" = "Rincian" "transportConfig" = "Transport" "expireDate" = "Durasi" +"createdAt" = "Dibuat" +"updatedAt" = "Diperbarui" "resetTraffic" = "Reset Traffic" "addInbound" = "Tambahkan Masuk" "generalActions" = "Tindakan Umum" diff --git a/web/translation/translate.ja_JP.toml b/web/translation/translate.ja_JP.toml index 8efa9074..a8702cd8 100644 --- a/web/translation/translate.ja_JP.toml +++ b/web/translation/translate.ja_JP.toml @@ -165,6 +165,8 @@ "details" = "詳細情報" "transportConfig" = "トランスポート設定" "expireDate" = "有効期限" +"createdAt" = "作成" +"updatedAt" = "更新" "resetTraffic" = "トラフィックリセット" "addInbound" = "インバウンド追加" "generalActions" = "一般操作" diff --git a/web/translation/translate.pt_BR.toml b/web/translation/translate.pt_BR.toml index 0d012591..813a4dde 100644 --- a/web/translation/translate.pt_BR.toml +++ b/web/translation/translate.pt_BR.toml @@ -165,6 +165,8 @@ "details" = "Detalhes" "transportConfig" = "Transporte" "expireDate" = "Duração" +"createdAt" = "Criado" +"updatedAt" = "Atualizado" "resetTraffic" = "Redefinir Tráfego" "addInbound" = "Adicionar Inbound" "generalActions" = "Ações Gerais" diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index 558d309b..be4a1ef3 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -165,6 +165,8 @@ "details" = "Подробнее" "transportConfig" = "Транспорт" "expireDate" = "Дата окончания" +"createdAt" = "Создано" +"updatedAt" = "Обновлено" "resetTraffic" = "Сброс трафика" "addInbound" = "Создать инбаунд" "generalActions" = "Общие действия" diff --git a/web/translation/translate.tr_TR.toml b/web/translation/translate.tr_TR.toml index 32dca2ea..7159c9b5 100644 --- a/web/translation/translate.tr_TR.toml +++ b/web/translation/translate.tr_TR.toml @@ -165,6 +165,8 @@ "details" = "Detaylar" "transportConfig" = "Taşıma" "expireDate" = "Süre" +"createdAt" = "Oluşturuldu" +"updatedAt" = "Güncellendi" "resetTraffic" = "Trafiği Sıfırla" "addInbound" = "Gelen Ekle" "generalActions" = "Genel Eylemler" diff --git a/web/translation/translate.uk_UA.toml b/web/translation/translate.uk_UA.toml index 8976b66a..95eca7a6 100644 --- a/web/translation/translate.uk_UA.toml +++ b/web/translation/translate.uk_UA.toml @@ -165,6 +165,8 @@ "details" = "Деталі" "transportConfig" = "Транспорт" "expireDate" = "Тривалість" +"createdAt" = "Створено" +"updatedAt" = "Оновлено" "resetTraffic" = "Скинути трафік" "addInbound" = "Додати вхідний" "generalActions" = "Загальні дії" diff --git a/web/translation/translate.vi_VN.toml b/web/translation/translate.vi_VN.toml index 6e28a0de..f8144a2f 100644 --- a/web/translation/translate.vi_VN.toml +++ b/web/translation/translate.vi_VN.toml @@ -165,6 +165,8 @@ "details" = "Chi tiết" "transportConfig" = "Giao vận" "expireDate" = "Ngày hết hạn" +"createdAt" = "Tạo lúc" +"updatedAt" = "Cập nhật" "resetTraffic" = "Đặt lại lưu lượng" "addInbound" = "Thêm điểm vào" "generalActions" = "Hành động chung" diff --git a/web/translation/translate.zh_CN.toml b/web/translation/translate.zh_CN.toml index a2142bd1..6490372c 100644 --- a/web/translation/translate.zh_CN.toml +++ b/web/translation/translate.zh_CN.toml @@ -165,6 +165,8 @@ "details" = "详细信息" "transportConfig" = "传输配置" "expireDate" = "到期时间" +"createdAt" = "创建时间" +"updatedAt" = "更新时间" "resetTraffic" = "重置流量" "addInbound" = "添加入站" "generalActions" = "通用操作" diff --git a/web/translation/translate.zh_TW.toml b/web/translation/translate.zh_TW.toml index db5b33ed..cd4f22e7 100644 --- a/web/translation/translate.zh_TW.toml +++ b/web/translation/translate.zh_TW.toml @@ -165,6 +165,8 @@ "details" = "詳細資訊" "transportConfig" = "傳輸配置" "expireDate" = "到期時間" +"createdAt" = "建立時間" +"updatedAt" = "更新時間" "resetTraffic" = "重置流量" "addInbound" = "新增入站" "generalActions" = "通用操作" From 3087c1b123f426b7c1306ab634fb84e7943e4217 Mon Sep 17 00:00:00 2001 From: Ali Golzar <57574919+aliglzr@users.noreply.github.com> Date: Thu, 28 Aug 2025 02:40:50 +0330 Subject: [PATCH 11/27] Add all-time traffic for inbounds and clients (#3387) * feat(db): add allTime field to Inbound and ClientTraffic models * feat(inbound): increment all_time for inbounds and clients on traffic updates calculate correct all_time traffic on migrate command * feat(ui): show all-time traffic column for inbounds and its clients * i18n: add pages.inbounds.allTimeTraffic label across locales * Add All Time Traffic Usage in inbounds page top banner --- database/model/model.go | 1 + web/assets/js/model/dbinbound.js | 1 + web/html/component/aClientTable.html | 4 ++++ web/html/inbounds.html | 34 ++++++++++++++++++++++++---- web/service/inbound.go | 26 +++++++++++++++++++-- web/translation/translate.ar_EG.toml | 2 ++ web/translation/translate.en_US.toml | 2 ++ web/translation/translate.es_ES.toml | 2 ++ web/translation/translate.fa_IR.toml | 2 ++ web/translation/translate.id_ID.toml | 2 ++ web/translation/translate.ja_JP.toml | 2 ++ web/translation/translate.pt_BR.toml | 2 ++ web/translation/translate.ru_RU.toml | 2 ++ web/translation/translate.tr_TR.toml | 2 ++ web/translation/translate.uk_UA.toml | 2 ++ web/translation/translate.vi_VN.toml | 2 ++ web/translation/translate.zh_CN.toml | 2 ++ web/translation/translate.zh_TW.toml | 2 ++ xray/client_traffic.go | 1 + 19 files changed, 86 insertions(+), 7 deletions(-) diff --git a/database/model/model.go b/database/model/model.go index 86ab0487..62cfac41 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -32,6 +32,7 @@ type Inbound struct { Up int64 `json:"up" form:"up"` Down int64 `json:"down" form:"down"` Total int64 `json:"total" form:"total"` + AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"` Remark string `json:"remark" form:"remark"` Enable bool `json:"enable" form:"enable"` ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` diff --git a/web/assets/js/model/dbinbound.js b/web/assets/js/model/dbinbound.js index 45301ddd..acb62ce4 100644 --- a/web/assets/js/model/dbinbound.js +++ b/web/assets/js/model/dbinbound.js @@ -6,6 +6,7 @@ class DBInbound { this.up = 0; this.down = 0; this.total = 0; + this.allTime = 0; this.remark = ""; this.enable = true; this.expiryTime = 0; diff --git a/web/html/component/aClientTable.html b/web/html/component/aClientTable.html index 359e6e74..53ec27a3 100644 --- a/web/html/component/aClientTable.html +++ b/web/html/component/aClientTable.html @@ -98,6 +98,10 @@ + +