1
0
Эх сурвалжийг харах

🚀 Some improvements for x-ui.sh and ip job (#665)

Hamidreza 1 жил өмнө
parent
commit
1028319386

+ 7 - 0
DockerEntrypoint.sh

@@ -0,0 +1,7 @@
+#!/bin/sh
+
+# Start fail2ban
+fail2ban-client -x -f start
+
+# Run x-ui
+exec /app/x-ui

+ 18 - 12
DockerInit.sh

@@ -1,22 +1,28 @@
 #!/bin/sh
-if [ $1 == "amd64" ]; then
-    ARCH="64";
-    FNAME="amd64";
-elif [ $1 == "arm64" ]; then
-    ARCH="arm64-v8a"
-    FNAME="arm64";
-else
-    ARCH="64";
-    FNAME="amd64";
-fi
+
+case $1 in
+    amd64)
+        ARCH="64"
+        FNAME="amd64"
+        ;;
+    arm64)
+        ARCH="arm64-v8a"
+        FNAME="arm64"
+        ;;
+    *)
+        ARCH="64"
+        FNAME="amd64"
+        ;;
+esac
+
 mkdir -p build/bin
 cd build/bin
+
 wget "https://github.com/mhsanaei/xray-core/releases/latest/download/Xray-linux-${ARCH}.zip"
 unzip "Xray-linux-${ARCH}.zip"
 rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat iran.dat
 mv xray "xray-linux-${FNAME}"
+
 wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
 wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
 wget "https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat"
-
-cd ../../

+ 35 - 8
Dockerfile

@@ -1,20 +1,47 @@
-#Build latest x-ui from source
+# ========================================================
+# Stage: Builder
+# ========================================================
 FROM --platform=$BUILDPLATFORM golang:1.20.4-alpine AS builder
 WORKDIR /app
-ARG TARGETARCH 
-RUN apk --no-cache --update add build-base gcc wget unzip
+ARG TARGETARCH
+ENV CGO_ENABLED=1
+
+RUN apk --no-cache --update add \
+  build-base \
+  gcc \
+  wget \
+  unzip
+
 COPY . .
-RUN env CGO_ENABLED=1 go build -o build/x-ui main.go
-RUN ./DockerInit.sh "$TARGETARCH"
 
+RUN go build -o build/x-ui main.go
+RUN ./DockerInit.sh "$TARGETARCH"
 
-#Build app image using latest x-ui
+# ========================================================
+# Stage: Final Image of 3x-ui
+# ========================================================
 FROM alpine
 ENV TZ=Asia/Tehran
 WORKDIR /app
 
-RUN apk add ca-certificates tzdata
+RUN apk add --no-cache --update \
+  ca-certificates \
+  tzdata \
+  fail2ban
 
 COPY --from=builder  /app/build/ /app/
+COPY --from=builder  /app/DockerEntrypoint.sh /app/
+COPY --from=builder  /app/x-ui.sh /usr/bin/x-ui
+
+# Configure fail2ban
+RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \
+  && cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local \
+  && sed -i "s/^\[ssh\]$/&\nenabled = false/" /etc/fail2ban/jail.local
+
+RUN chmod +x \
+  /app/DockerEntrypoint.sh \
+  /app/x-ui \
+  /usr/bin/x-ui
+
 VOLUME [ "/etc/x-ui" ]
-ENTRYPOINT [ "/app/x-ui" ]
+ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]

+ 2 - 1
README.md

@@ -178,7 +178,7 @@ If you want to use routing to WARP follow steps as below:
 2. Install WARP on **socks proxy mode**:
 
    ```sh
-   bash <(curl -sSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh)
+   bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
    ```
 
 3. Turn on the config you need in panel or [Copy and paste this file to Xray Configuration](./media/configs/traffic+block-ads+warp.json)
@@ -280,6 +280,7 @@ Reference syntax:
 | XUI_DEBUG      |                   `boolean`                    | `false`       |
 | XUI_BIN_FOLDER |                    `string`                    | `"bin"`       |
 | XUI_DB_FOLDER  |                    `string`                    | `"/etc/x-ui"` |
+| XUI_LOG_FOLDER |                    `string`                    | `"/var/log"`  |
 
 Example:
 

+ 8 - 0
config/config.go

@@ -65,3 +65,11 @@ func GetDBFolderPath() string {
 func GetDBPath() string {
 	return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
 }
+
+func GetLogFolder() string {
+	logFolderPath := os.Getenv("XUI_LOG_FOLDER")
+	if logFolderPath == "" {
+		logFolderPath = "/var/log"
+	}
+	return logFolderPath
+}

+ 3 - 1
database/db.go

@@ -6,6 +6,7 @@ import (
 	"io/fs"
 	"os"
 	"path"
+
 	"x-ui/config"
 	"x-ui/database/model"
 	"x-ui/xray"
@@ -26,7 +27,6 @@ var initializers = []func() error{
 }
 
 func initUser() error {
-
 	err := db.AutoMigrate(&model.User{})
 	if err != nil {
 		return err
@@ -54,9 +54,11 @@ func initInbound() error {
 func initSetting() error {
 	return db.AutoMigrate(&model.Setting{})
 }
+
 func initInboundClientIps() error {
 	return db.AutoMigrate(&model.InboundClientIps{})
 }
+
 func initClientTraffic() error {
 	return db.AutoMigrate(&xray.ClientTraffic{})
 }

+ 4 - 4
install.sh

@@ -8,7 +8,7 @@ plain='\033[0m'
 cur_dir=$(pwd)
 
 # check root
-[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error${plain} Please run this script with root privilege \n " && exit 1
+[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1
 
 # Check OS and set release variable
 if [[ -f /etc/os-release ]]; then
@@ -41,12 +41,12 @@ if [[ "${release}" == "centos" ]]; then
     fi
 elif [[ "${release}" == "ubuntu" ]]; then
     if [[ ${os_version} -lt 20 ]]; then
-        echo -e "${red}please use Ubuntu 20 or higher version${plain}\n" && exit 1
+        echo -e "${red}please use Ubuntu 20 or higher version!${plain}\n" && exit 1
     fi
 
 elif [[ "${release}" == "fedora" ]]; then
     if [[ ${os_version} -lt 36 ]]; then
-        echo -e "${red}please use Fedora 36 or higher version${plain}\n" && exit 1
+        echo -e "${red}please use Fedora 36 or higher version!${plain}\n" && exit 1
     fi
 
 elif [[ "${release}" == "debian" ]]; then
@@ -68,7 +68,7 @@ install_base() {
     esac
 }
 
-#This function will be called when user installed x-ui out of sercurity
+# This function will be called when user installed x-ui out of sercurity
 config_after_install() {
     echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
     read -p "Do you want to continue with the modification [y/n]? ": config_confirm

+ 2 - 1
web/controller/inbound.go

@@ -3,6 +3,7 @@ package controller
 import (
 	"fmt"
 	"strconv"
+
 	"x-ui/database/model"
 	"x-ui/logger"
 	"x-ui/web/global"
@@ -40,7 +41,6 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
 	g.POST("/resetAllTraffics", a.resetAllTraffics)
 	g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
 	g.POST("/delDepletedClients/:id", a.delDepletedClients)
-
 }
 
 func (a *InboundController) startTask() {
@@ -79,6 +79,7 @@ func (a *InboundController) getInbound(c *gin.Context) {
 	}
 	jsonObj(c, inbound, nil)
 }
+
 func (a *InboundController) getClientTraffics(c *gin.Context) {
 	email := c.Param("email")
 	clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)

+ 3 - 1
web/html/xui/inbound_client_table.html

@@ -45,7 +45,9 @@
         <a-tag :color="statsColor(record, client.email)">
             [[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] /
             <template v-if="client.totalGB > 0">[[client._totalGB]]GB</template>
-            <template v-else>♾</template>
+            <template v-else>
+                <svg style="fill: currentColor; height: 16px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"/></svg>
+            </template>
         </a-tag>
     </a-popover>
 </template>                                    

+ 3 - 1
web/html/xui/inbounds.html

@@ -224,7 +224,9 @@
                                         <template v-if="dbInbound.total > 0">
                                             [[ sizeFormat(dbInbound.total) ]]
                                         </template>
-                                        <template v-else>♾</template>
+                                        <template v-else>
+                                            <svg style="fill: currentColor; height: 16px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"/></svg>
+                                        </template>
                                     </a-tag>
                                 </a-popover>
                             </template>

+ 69 - 96
web/job/check_client_ip_job.go

@@ -5,23 +5,26 @@ import (
 	"log"
 	"os"
 	"regexp"
+	"sort"
+	"strings"
+	"time"
+
 	"x-ui/database"
 	"x-ui/database/model"
 	"x-ui/logger"
-	"x-ui/web/service"
 	"x-ui/xray"
-
-	"sort"
-	"strings"
-	"time"
 )
 
-type CheckClientIpJob struct {
-	xrayService service.XrayService
-}
+type CheckClientIpJob struct {}
 
 var job *CheckClientIpJob
 var disAllowedIps []string
+var ipFiles = []string{
+	xray.GetBlockedIPsPath(),
+	xray.GetIPLimitLogPath(),
+	xray.GetIPLimitBannedLogPath(),
+	xray.GetAccessPersistentLogPath(),
+}
 
 func NewCheckClientIpJob() *CheckClientIpJob {
 	job = new(CheckClientIpJob)
@@ -31,37 +34,28 @@ func NewCheckClientIpJob() *CheckClientIpJob {
 func (j *CheckClientIpJob) Run() {
 	logger.Debug("Check Client IP Job...")
 
-	if hasLimitIp() {
-		//create log file for Fail2ban IP Limit
-		logIpFile, err := os.OpenFile("/var/log/3xipl.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
-		checkError(err)
-		defer logIpFile.Close()
-		log.SetOutput(logIpFile)
-		log.SetFlags(log.LstdFlags)
-
-		//create file to collect access.log to another file accessp.log (p=persistent)
-		logAccessP, err := os.OpenFile("/usr/local/x-ui/accessp.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
-		checkError(err)
-		defer logAccessP.Close()
+	// create files required for iplimit if not exists
+	for i := 0; i < len(ipFiles); i++ {
+		file, err := os.OpenFile(ipFiles[i], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
+		j.checkError(err)
+		defer file.Close()
+	}
 
-		processLogFile()
+	// check for limit ip
+	if j.hasLimitIp() {
+		j.processLogFile()
 	}
 
+	// write to blocked ips
 	blockedIps := []byte(strings.Join(disAllowedIps, ","))
-
-	// check if file exists, if not create one
-	_, err := os.Stat(xray.GetBlockedIPsPath())
-	if os.IsNotExist(err) {
-		_, err = os.OpenFile(xray.GetBlockedIPsPath(), os.O_RDWR|os.O_CREATE, 0755)
-		checkError(err)
-	}
-	err = os.WriteFile(xray.GetBlockedIPsPath(), blockedIps, 0755)
-	checkError(err)
+	err := os.WriteFile(xray.GetBlockedIPsPath(), blockedIps, 0644)
+	j.checkError(err)
 }
 
-func hasLimitIp() bool {
+func (j *CheckClientIpJob) hasLimitIp() bool {
 	db := database.GetDB()
 	var inbounds []*model.Inbound
+
 	err := db.Model(model.Inbound{}).Find(&inbounds).Error
 	if err != nil {
 		return false
@@ -83,11 +77,12 @@ func hasLimitIp() bool {
 			}
 		}
 	}
+
 	return false
 }
 
-func processLogFile() {
-	accessLogPath := GetAccessLogPath()
+func (j *CheckClientIpJob) processLogFile() {
+	accessLogPath := xray.GetAccessLogPath()
 	if accessLogPath == "" {
 		logger.Warning("access.log doesn't exist in your config.json")
 		return
@@ -95,7 +90,7 @@ func processLogFile() {
 
 	data, err := os.ReadFile(accessLogPath)
 	InboundClientIps := make(map[string][]string)
-	checkError(err)
+	j.checkError(err)
 
 	lines := strings.Split(string(data), "\n")
 	for _, line := range lines {
@@ -116,7 +111,7 @@ func processLogFile() {
 			matchesEmail = strings.TrimSpace(strings.Split(matchesEmail, "email: ")[1])
 
 			if InboundClientIps[matchesEmail] != nil {
-				if contains(InboundClientIps[matchesEmail], ip) {
+				if j.contains(InboundClientIps[matchesEmail], ip) {
 					continue
 				}
 				InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
@@ -125,68 +120,50 @@ func processLogFile() {
 				InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
 			}
 		}
-
 	}
+
 	disAllowedIps = []string{}
 	shouldCleanLog := false
 
 	for clientEmail, ips := range InboundClientIps {
-		inboundClientIps, err := GetInboundClientIps(clientEmail)
+		inboundClientIps, err := j.getInboundClientIps(clientEmail)
 		sort.Strings(ips)
 		if err != nil {
-			addInboundClientIps(clientEmail, ips)
-
+			j.addInboundClientIps(clientEmail, ips)
 		} else {
-			shouldCleanLog = updateInboundClientIps(inboundClientIps, clientEmail, ips)
+			shouldCleanLog = j.updateInboundClientIps(inboundClientIps, clientEmail, ips)
 		}
 
 	}
 
+	// added 3 seconds delay before cleaning logs to reduce chance of logging IP that already has been banned
 	time.Sleep(time.Second * 3)
-	//added 3 seconds delay before cleaning logs to reduce chance of logging IP that already has been banned
+
 	if shouldCleanLog {
-		//copy log
-		logAccessP, err := os.OpenFile("/usr/local/x-ui/accessp.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
-		checkError(err)
+		// copy access log to persistent file
+		logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
+		j.checkError(err)
 		input, err := os.ReadFile(accessLogPath)
-		checkError(err)
+		j.checkError(err)
 		if _, err := logAccessP.Write(input); err != nil {
-			checkError(err)
+			j.checkError(err)
 		}
 		defer logAccessP.Close()
-		// clean log
-		if err := os.Truncate(GetAccessLogPath(), 0); err != nil {
-			checkError(err)
-		}
-	}
 
-}
-func GetAccessLogPath() string {
-
-	config, err := os.ReadFile(xray.GetConfigPath())
-	checkError(err)
-
-	jsonConfig := map[string]interface{}{}
-	err = json.Unmarshal([]byte(config), &jsonConfig)
-	checkError(err)
-	if jsonConfig["log"] != nil {
-		jsonLog := jsonConfig["log"].(map[string]interface{})
-		if jsonLog["access"] != nil {
-
-			accessLogPath := jsonLog["access"].(string)
-
-			return accessLogPath
+		// clean access log
+		if err := os.Truncate(xray.GetAccessLogPath(), 0); err != nil {
+			j.checkError(err)
 		}
 	}
-	return ""
-
 }
-func checkError(e error) {
+
+func (j *CheckClientIpJob) checkError(e error) {
 	if e != nil {
 		logger.Warning("client ip job err:", e)
 	}
 }
-func contains(s []string, str string) bool {
+
+func (j *CheckClientIpJob) contains(s []string, str string) bool {
 	for _, v := range s {
 		if v == str {
 			return true
@@ -195,7 +172,8 @@ func contains(s []string, str string) bool {
 
 	return false
 }
-func GetInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
+
+func (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
 	db := database.GetDB()
 	InboundClientIps := &model.InboundClientIps{}
 	err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error
@@ -204,10 +182,11 @@ func GetInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
 	}
 	return InboundClientIps, nil
 }
-func addInboundClientIps(clientEmail string, ips []string) error {
+
+func (j *CheckClientIpJob) addInboundClientIps(clientEmail string, ips []string) error {
 	inboundClientIps := &model.InboundClientIps{}
 	jsonIps, err := json.Marshal(ips)
-	checkError(err)
+	j.checkError(err)
 
 	inboundClientIps.ClientEmail = clientEmail
 	inboundClientIps.Ips = string(jsonIps)
@@ -229,17 +208,17 @@ func addInboundClientIps(clientEmail string, ips []string) error {
 	}
 	return nil
 }
-func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) bool {
 
+func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) bool {
 	jsonIps, err := json.Marshal(ips)
-	checkError(err)
+	j.checkError(err)
 
 	inboundClientIps.ClientEmail = clientEmail
 	inboundClientIps.Ips = string(jsonIps)
 
 	// check inbound limitation
-	inbound, err := GetInboundByEmail(clientEmail)
-	checkError(err)
+	inbound, err := j.getInboundByEmail(clientEmail)
+	j.checkError(err)
 
 	if inbound.Settings == "" {
 		logger.Debug("wrong data ", inbound)
@@ -251,13 +230,20 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
 	clients := settings["clients"]
 	shouldCleanLog := false
 
+	// create iplimit log file channel
+	logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
+	if err != nil {
+		logger.Errorf("failed to create or open ip limit log file: %s", err)
+	}
+	defer logIpFile.Close()
+	log.SetOutput(logIpFile)
+	log.SetFlags(log.LstdFlags)
+
 	for _, client := range clients {
 		if client.Email == clientEmail {
-
 			limitIp := client.LimitIP
 
 			if limitIp != 0 {
-
 				shouldCleanLog = true
 
 				if limitIp < len(ips) && inbound.Enable {
@@ -280,27 +266,14 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
 	return shouldCleanLog
 }
 
-func DisableInbound(id int) error {
-	db := database.GetDB()
-	result := db.Model(model.Inbound{}).
-		Where("id = ? and enable = ?", id, true).
-		Update("enable", false)
-	err := result.Error
-	logger.Warning("disable inbound with id:", id)
-
-	if err == nil {
-		job.xrayService.SetToNeedRestart()
-	}
-
-	return err
-}
-
-func GetInboundByEmail(clientEmail string) (*model.Inbound, error) {
+func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) {
 	db := database.GetDB()
 	var inbounds *model.Inbound
+
 	err := db.Model(model.Inbound{}).Where("settings LIKE ?", "%"+clientEmail+"%").Find(&inbounds).Error
 	if err != nil {
 		return nil, err
 	}
+
 	return inbounds, nil
 }

+ 25 - 0
web/job/clear_logs_job.go

@@ -0,0 +1,25 @@
+package job
+
+import (
+	"os"
+	"x-ui/logger"
+	"x-ui/xray"
+)
+
+type ClearLogsJob struct{}
+
+func NewClearLogsJob() *ClearLogsJob {
+	return new(ClearLogsJob)
+}
+
+// Here Run is an interface method of the Job interface
+func (j *ClearLogsJob) Run() {
+	logFiles := []string{xray.GetIPLimitLogPath(), xray.GetIPLimitBannedLogPath(), xray.GetAccessPersistentLogPath()}
+
+	// clear log files
+	for i := 0; i < len(logFiles); i++ {
+		if err := os.Truncate(logFiles[i], 0); err != nil {
+			logger.Warning("clear logs job err:", err)
+		}
+	}
+}

+ 4 - 8
web/service/inbound.go

@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"strings"
 	"time"
+
 	"x-ui/database"
 	"x-ui/database/model"
 	"x-ui/logger"
@@ -74,7 +75,6 @@ func (s *InboundService) getAllEmails() ([]string, error) {
 		FROM inbounds,
 			JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
 		`).Scan(&emails).Error
-
 	if err != nil {
 		return nil, err
 	}
@@ -816,7 +816,8 @@ func (s *InboundService) UpdateClientStat(email string, client *model.Client) er
 			"enable":      true,
 			"email":       client.Email,
 			"total":       client.TotalGB,
-			"expiry_time": client.ExpiryTime})
+			"expiry_time": client.ExpiryTime,
+		})
 	err := result.Error
 	if err != nil {
 		return err
@@ -1068,8 +1069,8 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
 		return err
 	}
 	return nil
-
 }
+
 func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry_time int64) error {
 	_, inbound, err := s.GetClientInboundByEmail(clientEmail)
 	if err != nil {
@@ -1126,7 +1127,6 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
 		return err
 	}
 	return nil
-
 }
 
 func (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error {
@@ -1137,7 +1137,6 @@ func (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error {
 		Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
 
 	err := result.Error
-
 	if err != nil {
 		return err
 	}
@@ -1209,7 +1208,6 @@ func (s *InboundService) ResetAllClientTraffics(id int) error {
 		Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
 
 	err := result.Error
-
 	if err != nil {
 		return err
 	}
@@ -1224,7 +1222,6 @@ func (s *InboundService) ResetAllTraffics() error {
 		Updates(map[string]interface{}{"up": 0, "down": 0})
 
 	err := result.Error
-
 	if err != nil {
 		return err
 	}
@@ -1411,7 +1408,6 @@ func (s *InboundService) ClearClientIps(clientEmail string) error {
 		Where("client_email = ?", clientEmail).
 		Update("ips", "")
 	err := result.Error
-
 	if err != nil {
 		return err
 	}

+ 1 - 3
web/service/server.go

@@ -14,6 +14,7 @@ import (
 	"runtime"
 	"strings"
 	"time"
+
 	"x-ui/config"
 	"x-ui/database"
 	"x-ui/logger"
@@ -250,7 +251,6 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
 }
 
 func (s *ServerService) StopXrayService() (string error) {
-
 	err := s.xrayService.StopXray()
 	if err != nil {
 		logger.Error("stop xray failed:", err)
@@ -261,7 +261,6 @@ func (s *ServerService) StopXrayService() (string error) {
 }
 
 func (s *ServerService) RestartXrayService() (string error) {
-
 	s.xrayService.StopXray()
 	defer func() {
 		err := s.xrayService.RestartXray(true)
@@ -377,7 +376,6 @@ func (s *ServerService) UpdateXray(version string) error {
 	}
 
 	return nil
-
 }
 
 func (s *ServerService) GetLogs(count string, logLevel string) ([]string, error) {

+ 3 - 0
web/web.go

@@ -253,6 +253,9 @@ func (s *Server) startTask() {
 	// check client ips from log file every 20 sec
 	s.cron.AddJob("@every 20s", job.NewCheckClientIpJob())
 
+	// check client ips from log file every 3 day
+	s.cron.AddJob("@every 3d", job.NewClearLogsJob())
+
 	// Make a traffic condition every day, 8:30
 	var entry cron.EntryID
 	isTgbotenabled, err := s.settingService.GetTgbotenabled()

+ 160 - 115
x-ui.sh

@@ -56,6 +56,13 @@ elif [[ "${release}" == "debian" ]]; then
     fi
 fi
 
+
+# Declare Variables
+log_folder="${XUI_LOG_FOLDER:=/var/log}"
+iplimit_log_path="${log_folder}/3xipl.log"
+iplimit_banned_log_path="${log_folder}/3xipl-banned.log"
+
+
 confirm() {
     if [[ $# > 1 ]]; then
         echo && read -p "$1 [Default $2]: " temp
@@ -296,25 +303,28 @@ enable_bbr() {
     fi
 
     # Check the OS and install necessary packages
-    if [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "ubuntu" ]]; then
-        sudo apt-get update && sudo apt-get install -yqq --no-install-recommends ca-certificates
-    elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "debian" ]]; then
-        sudo apt-get update && sudo apt-get install -yqq --no-install-recommends ca-certificates
-    elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "fedora" ]]; then
-        sudo dnf -y update && sudo dnf -y install ca-certificates
-    elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "centos" ]]; then
-        sudo yum -y update && sudo yum -y install ca-certificates
-    else
-        echo "Unsupported operating system. Please check the script and install the necessary packages manually."
-        exit 1
-    fi
+    case "${release}" in
+        ubuntu|debian)
+            apt-get update && apt-get install -yqq --no-install-recommends ca-certificates
+            ;;
+        centos)
+            yum -y update && yum -y install ca-certificates
+            ;;
+        fedora)
+            dnf -y update && dnf -y install ca-certificates
+            ;;
+        *)
+            echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
+            exit 1
+            ;;
+    esac
 
     # Enable BBR
-    echo "net.core.default_qdisc=fq" | sudo tee -a /etc/sysctl.conf
-    echo "net.ipv4.tcp_congestion_control=bbr" | sudo tee -a /etc/sysctl.conf
+    echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf
+    echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf
 
     # Apply changes
-    sudo sysctl -p
+    sysctl -p
 
     # Verify that BBR is enabled
     if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "bbr" ]]; then
@@ -434,24 +444,24 @@ show_xray_status() {
 open_ports() {
     if ! command -v ufw &>/dev/null; then
         echo "ufw firewall is not installed. Installing now..."
-        sudo apt-get update
-        sudo apt-get install -y ufw
+        apt-get update
+        apt-get install -y ufw
     else
         echo "ufw firewall is already installed"
     fi
 
     # Check if the firewall is inactive
-    if sudo ufw status | grep -q "Status: active"; then
+    if ufw status | grep -q "Status: active"; then
         echo "firewall is already active"
     else
         # Open the necessary ports
-        sudo ufw allow ssh
-        sudo ufw allow http
-        sudo ufw allow https
-        sudo ufw allow 2053/tcp
+        ufw allow ssh
+        ufw allow http
+        ufw allow https
+        ufw allow 2053/tcp
 
         # Enable the firewall
-        sudo ufw --force enable
+        ufw --force enable
     fi
 
     # Prompt the user to enter a list of ports
@@ -472,15 +482,15 @@ open_ports() {
             end_port=$(echo $port | cut -d'-' -f2)
             # Loop through the range and open each port
             for ((i = start_port; i <= end_port; i++)); do
-                sudo ufw allow $i
+                ufw allow $i
             done
         else
-            sudo ufw allow "$port"
+            ufw allow "$port"
         fi
     done
 
     # Confirm that the ports are open
-    sudo ufw status | grep $ports
+    ufw status | grep $ports
 }
 
 update_geo() {
@@ -539,7 +549,7 @@ ssl_cert_issue_main() {
 }
 
 ssl_cert_issue() {
-    #check for acme.sh first
+    # check for acme.sh first
     if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
         echo "acme.sh could not be found. we will install it"
         install_acme
@@ -548,24 +558,30 @@ ssl_cert_issue() {
             exit 1
         fi
     fi
-    #install socat second
-    if [[ "${release}" == "centos" ]] || [[ "${release}" == "fedora" ]]; then
-        yum install socat -y
-    else
-        apt install socat -y
-    fi
+    # install socat second
+    case "${release}" in
+        ubuntu|debian)
+            apt update && apt install socat -y ;;
+        centos)
+            yum -y update && yum -y install socat ;;
+        fedora)
+            dnf -y update && dnf -y install socat ;;
+        *)
+            echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
+            exit 1 ;;
+    esac
     if [ $? -ne 0 ]; then
-        LOGE "install socat failed,please check logs"
+        LOGE "install socat failed, please check logs"
         exit 1
     else
         LOGI "install socat succeed..."
     fi
 
-    #get the domain here,and we need verify it
+    # get the domain here,and we need verify it
     local domain=""
     read -p "Please enter your domain name:" domain
     LOGD "your domain is:${domain},check it..."
-    #here we need to judge whether there exists cert already
+    # here we need to judge whether there exists cert already
     local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
 
     if [ ${currentCert} == ${domain} ]; then
@@ -577,7 +593,7 @@ ssl_cert_issue() {
         LOGI "your domain is ready for issuing cert now..."
     fi
 
-    #create a directory for install cert
+    # create a directory for install cert
     certPath="/root/cert/${domain}"
     if [ ! -d "$certPath" ]; then
         mkdir -p "$certPath"
@@ -586,15 +602,15 @@ ssl_cert_issue() {
         mkdir -p "$certPath"
     fi
 
-    #get needed port here
+    # get needed port here
     local WebPort=80
     read -p "please choose which port do you use,default will be 80 port:" WebPort
     if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then
         LOGE "your input ${WebPort} is invalid,will use default port"
     fi
     LOGI "will use port:${WebPort} to issue certs,please make sure this port is open..."
-    #NOTE:This should be handled by user
-    #open the port and kill the occupied progress
+    # NOTE:This should be handled by user
+    # open the port and kill the occupied progress
     ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
     ~/.acme.sh/acme.sh --issue -d ${domain} --standalone --httpport ${WebPort}
     if [ $? -ne 0 ]; then
@@ -604,7 +620,7 @@ ssl_cert_issue() {
     else
         LOGE "issue certs succeed,installing certs..."
     fi
-    #install cert
+    # install cert
     ~/.acme.sh/acme.sh --installcert -d ${domain} \
         --key-file /root/cert/${domain}/privkey.pem \
         --fullchain-file /root/cert/${domain}/fullchain.pem
@@ -628,18 +644,17 @@ ssl_cert_issue() {
         ls -lah cert/*
         chmod 755 $certPath/*
     fi
-
 }
 
 warp_cloudflare() {
-    echo -e "${green}\t1.${plain} install WARP"
+    echo -e "${green}\t1.${plain} Install WARP socks5 proxy"
     echo -e "${green}\t2.${plain} Account Type (free, plus, team)"
     echo -e "${green}\t3.${plain} Turn on/off WireProxy"
     echo -e "${green}\t4.${plain} Uninstall WARP"
     read -p "Choose an option: " choice
     case "$choice" in
         1) 
-            bash <(curl -sSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh)
+            bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
             ;;
         2) 
             warp a
@@ -679,8 +694,8 @@ run_speedtest() {
             echo "Error: Package manager not found. You may need to install Speedtest manually."
             return 1
         else
-            curl -s $speedtest_install_script | sudo bash
-            sudo $pkg_manager install -y speedtest
+            curl -s $speedtest_install_script | bash
+            $pkg_manager install -y speedtest
         fi
     fi
 
@@ -688,6 +703,70 @@ run_speedtest() {
     speedtest
 }
 
+create_iplimit_jails() {
+    # Use default bantime if not passed => 5 minutes
+    local bantime="${1:-5}"
+
+    cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf
+[3x-ipl]
+enabled=true
+filter=3x-ipl
+action=3x-ipl
+logpath=${iplimit_log_path}
+maxretry=3
+findtime=100
+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*<F-USER>.+</F-USER>\s*\|\|\s*SRC\s*=\s*<ADDR>
+ignoreregex =
+EOF
+
+    cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf
+[INCLUDES]
+before = iptables-common.conf
+
+[Definition]
+actionstart = <iptables> -N f2b-<name>
+              <iptables> -A f2b-<name> -j <returntype>
+              <iptables> -I <chain> -p <protocol> -j f2b-<name>
+
+actionstop = <iptables> -D <chain> -p <protocol> -j f2b-<name>
+             <actionflush>
+             <iptables> -X f2b-<name>
+
+actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \t]'
+
+actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
+            echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S")   BAN   [Email] = <F-USER> [IP] = <ip> banned for <bantime> seconds." >> ${iplimit_banned_log_path}
+
+actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
+              echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S")   UNBAN   [Email] = <F-USER> [IP] = <ip> unbanned." >> ${iplimit_banned_log_path}
+
+[Init]
+EOF
+
+    echo -e "${green}Created Ip Limit jail files 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
+}
+
 iplimit_main() {
     echo -e "\n${green}\t1.${plain} Install Fail2ban and configure IP Limit"
     echo -e "${green}\t2.${plain} Change Ban Duration"
@@ -699,24 +778,23 @@ iplimit_main() {
     case "$choice" in
         0)
             show_menu ;;
-        1) 
+        1)
             confirm "Proceed with installation of Fail2ban & IP Limit?" "y"
             if [[ $? == 0 ]]; then
                 install_iplimit
             else
                 iplimit_main
             fi ;;
-        2)  
+        2)
             read -rp "Please enter new Ban Duration in Minutes [default 5]: " NUM
             if [[ $NUM =~ ^[0-9]+$ ]]; then
-                echo -e "\n[3x-ipl]\nenabled=true\nfilter=3x-ipl\naction=3x-ipl\nlogpath=/var/log/3xipl.log\nmaxretry=3\nfindtime=100\nbantime=${NUM}m" > /etc/fail2ban/jail.d/3x-ipl.conf
-                sudo systemctl restart fail2ban
-                echo -e "${green}Bantime set to ${NUM} minutes successfully.${plain}"
+                create_iplimit_jail ${NUM}
+                systemctl restart fail2ban
             else
                 echo -e "${red}${NUM} is not a number! Please, try again.${plain}"
             fi
             iplimit_main ;;
-        3)  
+        3)
             confirm "Proceed with Unbanning everyone from IP Limit jail?" "y"
             if [[ $? == 0 ]]; then
                 fail2ban-client reload --restart --unban 3x-ipl
@@ -727,9 +805,9 @@ iplimit_main() {
             fi
             iplimit_main ;;
         4)
-            if test -f "/var/log/3xipl-banned.log"; then
-                if [[ -s "/var/log/3xipl-banned.log" ]]; then
-                    cat /var/log/3xipl-banned.log
+            if test -f "${iplimit_banned_log_path}"; then
+                if [[ -s "${iplimit_banned_log_path}" ]]; then
+                    cat ${iplimit_banned_log_path}
                 else
                     echo -e "${red}Log file is empty.${plain}\n"
                 fi
@@ -737,7 +815,7 @@ iplimit_main() {
                 echo -e "${red}Log file not found. Please Install Fail2ban and IP Limit first.${plain}\n"
                 iplimit_main
             fi ;;
-        5)  
+        5)
             remove_iplimit ;;
         *) echo "Invalid choice" ;;
     esac
@@ -749,11 +827,11 @@ install_iplimit() {
         # Check the OS and install necessary packages
         case "${release}" in
             ubuntu|debian)
-                sudo apt-get update && sudo apt-get install fail2ban -y ;;
+                apt update && apt install fail2ban -y ;;
             centos)
-                sudo yum -y update && sudo yum -y install fail2ban ;;
+                yum -y update && yum -y install fail2ban ;;
             fedora)
-                sudo dnf -y update && sudo dnf -y install fail2ban ;;
+                dnf -y update && dnf -y install fail2ban ;;
             *)
                 echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
                 exit 1 ;;
@@ -765,63 +843,30 @@ install_iplimit() {
 
     echo -e "${green}Configuring IP Limit...${plain}\n"
 
-    #Check if [3x-ipl] exists in jail.local (just making sure there's no double config for jail)
-    if grep -qw '3x-ipl' /etc/fail2ban/jail.local || grep -qw '3x-ipl' /etc/fail2ban/jail.conf; then
-        echo -e "${red}Found conflicts in /etc/fail2ban/jail.conf or jail.local file!\nPlease manually remove anything related 3x-ipl in that files and try again.\nInstallation of IP Limit failed.${plain}\n"
-        exit 1
-    fi
+    # make sure there's no conflict for jail files
+    iplimit_remove_conflicts
 
-    #Check if log file exists
-    if ! test -f "/var/log/3xipl-banned.log"; then
-        touch /var/log/3xipl-banned.log
+    # Check if log file exists
+    if ! test -f "${iplimit_banned_log_path}"; then
+        touch ${iplimit_banned_log_path}
     fi
 
-    #Check if service log file exists so fail2ban won't return error
-    if ! test -f "/var/log/3xipl.log"; then
-        touch /var/log/3xipl.log
+    # Check if service log file exists so fail2ban won't return error
+    if ! test -f "${iplimit_log_path}"; then
+        touch ${iplimit_log_path}
     fi
-    
-
-    echo -e "\n[3x-ipl]\nenabled=true\nfilter=3x-ipl\naction=3x-ipl\nlogpath=/var/log/3xipl.log\nmaxretry=3\nfindtime=100\nbantime=5m" > /etc/fail2ban/jail.d/3x-ipl.conf
-
-    sudo cat > /etc/fail2ban/filter.d/3x-ipl.conf << EOF 
-[Definition]
-datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S
-failregex   = \[LIMIT_IP\]\s*Email\s*=\s*<F-USER>.+</F-USER>\s*\|\|\s*SRC\s*=\s*<ADDR>
-ignoreregex =
-EOF
 
-    sudo cat > /etc/fail2ban/action.d/3x-ipl.conf << 'EOF'
-[INCLUDES]
-before = iptables-common.conf
-
-[Definition]
-actionstart = <iptables> -N f2b-<name>
-              <iptables> -A f2b-<name> -j <returntype>
-              <iptables> -I <chain> -p <protocol> -j f2b-<name>
-
-actionstop = <iptables> -D <chain> -p <protocol> -j f2b-<name>
-             <actionflush>
-             <iptables> -X f2b-<name>
-
-actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \t]'
-
-actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
-            echo "$(date +"%%Y/%%m/%%d %%H:%%M:%%S")   BAN   [Email] = <F-USER> [IP] = <ip> banned for <bantime> seconds." >> /var/log/3xipl-banned.log
-
-actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
-              echo "$(date +"%%Y/%%m/%%d %%H:%%M:%%S")   UNBAN   [Email] = <F-USER> [IP] = <ip> unbanned." >> /var/log/3xipl-banned.log
-
-[Init]
-EOF
+    # Create the iplimit jail files
+    # we didn't pass the bantime here to use the default value
+    create_iplimit_jails
 
-    #Launching fail2ban
-    if ! sudo systemctl is-active --quiet fail2ban; then
-        sudo systemctl start fail2ban
+    # Launching fail2ban
+    if ! systemctl is-active --quiet fail2ban; then
+        systemctl start fail2ban
     else
         systemctl restart fail2ban
     fi
-    sudo systemctl enable fail2ban
+    systemctl enable fail2ban
 
     echo -e "${green}IP Limit installed and configured successfully!${plain}\n"
     before_show_menu
@@ -837,27 +882,27 @@ remove_iplimit(){
             rm -f /etc/fail2ban/filter.d/3x-ipl.conf
             rm -f /etc/fail2ban/action.d/3x-ipl.conf
             rm -f /etc/fail2ban/jail.d/3x-ipl.conf
-            sudo systemctl restart fail2ban
+            systemctl restart fail2ban
             echo -e "${green}IP Limit removed successfully!${plain}\n"
             before_show_menu ;;
         2)  
             rm -f /etc/fail2ban/filter.d/3x-ipl.conf
             rm -f /etc/fail2ban/action.d/3x-ipl.conf
             rm -f /etc/fail2ban/jail.d/3x-ipl.conf
-            sudo systemctl stop fail2ban
-            sudo systemctl disable fail2ban
+            systemctl stop fail2ban
+            systemctl disable fail2ban
             case "${release}" in
                 ubuntu|debian)
-                    sudo apt-get remove fail2ban -y ;;
+                    apt remove fail2ban -y ;;
                 centos)
-                    sudo yum -y remove fail2ban ;;
+                    yum -y remove fail2ban ;;
                 fedora)
-                    sudo dnf -y remove fail2ban ;;
+                    dnf -y remove fail2ban ;;
                 *)
                     echo -e "${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\n"
                     exit 1 ;;
             esac
-            rm -rf /etc/fail2ban/*
+            rm -rf /etc/fail2ban
             echo -e "${green}Fail2ban and IP Limit removed successfully!${plain}\n"
             before_show_menu ;;
         0) 
@@ -917,7 +962,7 @@ show_menu() {
   ${green}19.${plain} Update Geo Files
   ${green}20.${plain} Active Firewall and open ports
   ${green}21.${plain} Speedtest by Ookla
- "
+"
     show_status
     echo && read -p "Please enter your selection [0-21]: " num
 

+ 38 - 0
xray/process.go

@@ -14,6 +14,7 @@ import (
 	"sync"
 	"syscall"
 	"x-ui/config"
+	"x-ui/logger"
 	"x-ui/util/common"
 
 	"github.com/Workiva/go-datastructures/queue"
@@ -47,10 +48,47 @@ func GetBlockedIPsPath() string {
 	return config.GetBinFolderPath() + "/BlockedIps"
 }
 
+func GetIPLimitLogPath() string {
+	return config.GetLogFolder() + "/3xipl.log"
+}
+
+func GetIPLimitBannedLogPath() string {
+	return config.GetLogFolder() + "/3xipl-banned.log"
+}
+
+func GetAccessPersistentLogPath() string {
+	return config.GetLogFolder() + "/3xipl-access-persistent.log"
+}
+
+func GetAccessLogPath() string {
+	config, err := os.ReadFile(GetConfigPath())
+	if err != nil {
+		logger.Warningf("Something went wrong: %s", err)
+	}
+
+	jsonConfig := map[string]interface{}{}
+	err = json.Unmarshal([]byte(config), &jsonConfig)
+	if err != nil {
+		logger.Warningf("Something went wrong: %s", err)
+	}
+
+	if jsonConfig["log"] != nil {
+		jsonLog := jsonConfig["log"].(map[string]interface{})
+		if jsonLog["access"] != nil {
+
+			accessLogPath := jsonLog["access"].(string)
+
+			return accessLogPath
+		}
+	}
+	return ""
+}
+
 func stopProcess(p *Process) {
 	p.Stop()
 }
 
+
 type Process struct {
 	*process
 }