diff --git a/DockerEntrypoint.sh b/DockerEntrypoint.sh new file mode 100644 index 00000000..26083c5d --- /dev/null +++ b/DockerEntrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Start fail2ban +fail2ban-client -x -f start + +# Run x-ui +exec /app/x-ui diff --git a/DockerInit.sh b/DockerInit.sh index 0b83457c..d1a5e40a 100755 --- a/DockerInit.sh +++ b/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 ../../ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 6114d548..9e30d9f4 100644 --- a/Dockerfile +++ b/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 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" ] diff --git a/README.md b/README.md index 9ba7e289..9f6325ed 100644 --- a/README.md +++ b/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: diff --git a/config/config.go b/config/config.go index 30f0ca81..70be5ae6 100644 --- a/config/config.go +++ b/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 +} diff --git a/database/db.go b/database/db.go index 97ef2160..aaeb8bd9 100644 --- a/database/db.go +++ b/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{}) } diff --git a/install.sh b/install.sh index 9e995095..653fe979 100644 --- a/install.sh +++ b/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 diff --git a/web/controller/inbound.go b/web/controller/inbound.go index a9699bbb..d0e9cc03 100644 --- a/web/controller/inbound.go +++ b/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) diff --git a/web/html/xui/inbound_client_table.html b/web/html/xui/inbound_client_table.html index 140d7626..0c393675 100644 --- a/web/html/xui/inbound_client_table.html +++ b/web/html/xui/inbound_client_table.html @@ -45,7 +45,9 @@ [[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] / - + diff --git a/web/html/xui/inbounds.html b/web/html/xui/inbounds.html index 3640c9bb..015ba192 100644 --- a/web/html/xui/inbounds.html +++ b/web/html/xui/inbounds.html @@ -224,7 +224,9 @@ - + diff --git a/web/job/check_client_ip_job.go b/web/job/check_client_ip_job.go index 758929e9..1c292a50 100644 --- a/web/job/check_client_ip_job.go +++ b/web/job/check_client_ip_job.go @@ -5,23 +5,26 @@ import ( "log" "os" "regexp" - "x-ui/database" - "x-ui/database/model" - "x-ui/logger" - "x-ui/web/service" - "x-ui/xray" - "sort" "strings" "time" + + "x-ui/database" + "x-ui/database/model" + "x-ui/logger" + "x-ui/xray" ) -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() - - processLogFile() + // 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() } + // 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) + + // clean access log + if err := os.Truncate(xray.GetAccessLogPath(), 0); err != nil { + j.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 - } - } - 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 } diff --git a/web/job/clear_logs_job.go b/web/job/clear_logs_job.go new file mode 100644 index 00000000..34f13eaa --- /dev/null +++ b/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) + } + } +} diff --git a/web/service/inbound.go b/web/service/inbound.go index 802e753d..b278eb57 100644 --- a/web/service/inbound.go +++ b/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 } diff --git a/web/service/server.go b/web/service/server.go index 936e6469..5216be5a 100644 --- a/web/service/server.go +++ b/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) { diff --git a/web/web.go b/web/web.go index b78ac2f0..14e06f67 100644 --- a/web/web.go +++ b/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() diff --git a/x-ui.sh b/x-ui.sh index 99bde08a..fc328f8e 100644 --- a/x-ui.sh +++ b/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,110 +703,29 @@ run_speedtest() { speedtest } -iplimit_main() { - echo -e "\n${green}\t1.${plain} Install Fail2ban and configure IP Limit" - echo -e "${green}\t2.${plain} Change Ban Duration" - echo -e "${green}\t3.${plain} Unban Everyone" - echo -e "${green}\t4.${plain} Check Logs" - echo -e "${green}\t5.${plain} Uninstall IP Limit" - echo -e "${green}\t0.${plain} Back to Main Menu" - read -p "Choose an option: " choice - case "$choice" in - 0) - show_menu ;; - 1) - confirm "Proceed with installation of Fail2ban & IP Limit?" "y" - if [[ $? == 0 ]]; then - install_iplimit - else - iplimit_main - fi ;; - 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}" - else - echo -e "${red}${NUM} is not a number! Please, try again.${plain}" - fi - iplimit_main ;; - 3) - confirm "Proceed with Unbanning everyone from IP Limit jail?" "y" - if [[ $? == 0 ]]; then - fail2ban-client reload --restart --unban 3x-ipl - echo -e "${green}All users Unbanned successfully.${plain}" - iplimit_main - else - echo -e "${yellow}Cancelled.${plain}" - 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 - else - echo -e "${red}Log file is empty.${plain}\n" - fi - else - echo -e "${red}Log file not found. Please Install Fail2ban and IP Limit first.${plain}\n" - iplimit_main - fi ;; - 5) - remove_iplimit ;; - *) echo "Invalid choice" ;; - esac -} +create_iplimit_jails() { + # Use default bantime if not passed => 5 minutes + local bantime="${1:-5}" -install_iplimit() { - if ! command -v fail2ban-client &>/dev/null; then - echo -e "${green}Fail2ban is not installed. Installing now...!${plain}\n" - # Check the OS and install necessary packages - case "${release}" in - ubuntu|debian) - sudo apt-get update && sudo apt-get install fail2ban -y ;; - centos) - sudo yum -y update && sudo yum -y install fail2ban ;; - fedora) - sudo dnf -y update && sudo dnf -y install fail2ban ;; - *) - echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" - exit 1 ;; - esac - echo -e "${green}Fail2ban installed successfully!${plain}\n" - else - echo -e "${yellow}Fail2ban is already installed.${plain}\n" - fi + 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 - 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 - - #Check if log file exists - if ! test -f "/var/log/3xipl-banned.log"; then - touch /var/log/3xipl-banned.log - 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 - 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 + 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 - sudo cat > /etc/fail2ban/action.d/3x-ipl.conf << 'EOF' + cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf [INCLUDES] before = iptables-common.conf @@ -807,21 +741,132 @@ actionstop = -D -p -j 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." >> /var/log/3xipl-banned.log + 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." >> /var/log/3xipl-banned.log + echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = [IP] = unbanned." >> ${iplimit_banned_log_path} [Init] EOF - #Launching fail2ban - if ! sudo systemctl is-active --quiet fail2ban; then - sudo systemctl start fail2ban + 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" + echo -e "${green}\t3.${plain} Unban Everyone" + echo -e "${green}\t4.${plain} Check Logs" + echo -e "${green}\t5.${plain} Uninstall IP Limit" + echo -e "${green}\t0.${plain} Back to Main Menu" + read -p "Choose an option: " choice + case "$choice" in + 0) + show_menu ;; + 1) + confirm "Proceed with installation of Fail2ban & IP Limit?" "y" + if [[ $? == 0 ]]; then + install_iplimit + else + iplimit_main + fi ;; + 2) + read -rp "Please enter new Ban Duration in Minutes [default 5]: " NUM + if [[ $NUM =~ ^[0-9]+$ ]]; then + create_iplimit_jail ${NUM} + systemctl restart fail2ban + else + echo -e "${red}${NUM} is not a number! Please, try again.${plain}" + fi + iplimit_main ;; + 3) + confirm "Proceed with Unbanning everyone from IP Limit jail?" "y" + if [[ $? == 0 ]]; then + fail2ban-client reload --restart --unban 3x-ipl + echo -e "${green}All users Unbanned successfully.${plain}" + iplimit_main + else + echo -e "${yellow}Cancelled.${plain}" + fi + iplimit_main ;; + 4) + 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 + else + echo -e "${red}Log file not found. Please Install Fail2ban and IP Limit first.${plain}\n" + iplimit_main + fi ;; + 5) + remove_iplimit ;; + *) echo "Invalid choice" ;; + esac +} + +install_iplimit() { + if ! command -v fail2ban-client &>/dev/null; then + echo -e "${green}Fail2ban is not installed. Installing now...!${plain}\n" + # Check the OS and install necessary packages + case "${release}" in + ubuntu|debian) + apt update && apt install fail2ban -y ;; + centos) + yum -y update && yum -y install fail2ban ;; + fedora) + 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 ;; + esac + echo -e "${green}Fail2ban installed successfully!${plain}\n" + else + echo -e "${yellow}Fail2ban is already installed.${plain}\n" + fi + + echo -e "${green}Configuring IP Limit...${plain}\n" + + # make sure there's no conflict for jail files + iplimit_remove_conflicts + + # 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 "${iplimit_log_path}"; then + touch ${iplimit_log_path} + fi + + # Create the iplimit jail files + # we didn't pass the bantime here to use the default value + create_iplimit_jails + + # 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 diff --git a/xray/process.go b/xray/process.go index 42b2ee5c..0882c716 100644 --- a/xray/process.go +++ b/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 }