package job import ( "bufio" "encoding/json" "io" "log" "os" "os/exec" "regexp" "sort" "strings" "sync" "time" "x-ui/config" "x-ui/database" "x-ui/database/model" "x-ui/logger" "x-ui/xray" ) type CheckClientIpJob struct { disAllowedIps []string } var job *CheckClientIpJob var ipFiles = []string{ xray.GetIPLimitLogPath(), xray.GetIPLimitBannedLogPath(), xray.GetIPLimitBannedPrevLogPath(), xray.GetAccessPersistentLogPath(), xray.GetAccessPersistentPrevLogPath(), } func NewCheckClientIpJob() *CheckClientIpJob { job = new(CheckClientIpJob) return job } func (j *CheckClientIpJob) Run() { var wg sync.WaitGroup if j.checkFail2BanInstalled() { j.openLogFiles(ipFiles) } if j.hasLimitIp() { if j.checkFail2BanInstalled() && xray.GetAccessLogPath() == "./access.log" { j.processLogFile() } else { if !j.checkFail2BanInstalled() { logger.Warning("fail2ban is not installed. IP limiting may not work properly.") } switch xray.GetAccessLogPath() { case "none": logger.Warning("Access log is set to 'none', check your Xray Configs") case "": logger.Warning("Access log doesn't exist in your Xray Configs") } } } if !j.checkFail2BanInstalled() && xray.GetAccessLogPath() == "./access.log" { wg.Add(1) go func() { defer wg.Done() j.clearLogTime() }() wg.Wait() } } func (j *CheckClientIpJob) clearLogTime() { ticker := time.NewTicker(time.Hour) defer ticker.Stop() for range ticker.C { j.clearAccessLog() } } func (j *CheckClientIpJob) clearAccessLog() { accessLogPath := xray.GetAccessLogPath() logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644) j.checkError(err) defer logAccessP.Close() // reopen the access log file for reading file, err := os.Open(accessLogPath) j.checkError(err) // copy access log content to persistent file _, err = io.Copy(logAccessP, file) j.checkError(err) // close the file after copying content file.Close() // clean access log err = os.Truncate(accessLogPath, 0) j.checkError(err) } 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 } for _, inbound := range inbounds { if inbound.Settings == "" { continue } settings := map[string][]model.Client{} json.Unmarshal([]byte(inbound.Settings), &settings) clients := settings["clients"] for _, client := range clients { limitIp := client.LimitIP if limitIp > 0 { return true } } } return false } func (j *CheckClientIpJob) checkFail2BanInstalled() bool { cmd := "fail2ban-client" args := []string{"-h"} err := exec.Command(cmd, args...).Run() return err == nil } func (j *CheckClientIpJob) openLogFiles(ipFiles []string) { for i := 0; i < len(ipFiles); i++ { err := os.MkdirAll(config.GetLogFolder(), 0770) j.checkError(err) file, err := os.OpenFile(ipFiles[i], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644) j.checkError(err) defer file.Close() } } func (j *CheckClientIpJob) processLogFile() { accessLogPath := xray.GetAccessLogPath() file, err := os.Open(accessLogPath) j.checkError(err) defer file.Close() InboundClientIps := make(map[string][]string) scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() ipRegx, _ := regexp.Compile(`(\d+\.\d+\.\d+\.\d+).* accepted`) emailRegx, _ := regexp.Compile(`email:.+`) matches := ipRegx.FindStringSubmatch(line) if len(matches) > 1 { ip := matches[1] if ip == "127.0.0.1" { continue } matchesEmail := emailRegx.FindString(line) if matchesEmail == "" { continue } matchesEmail = strings.TrimSpace(strings.Split(matchesEmail, "email: ")[1]) if InboundClientIps[matchesEmail] != nil { if j.contains(InboundClientIps[matchesEmail], ip) { continue } InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip) } else { InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip) } } } j.checkError(scanner.Err()) shouldCleanLog := false for clientEmail, ips := range InboundClientIps { inboundClientIps, err := j.getInboundClientIps(clientEmail) sort.Strings(ips) if err != nil { j.addInboundClientIps(clientEmail, ips) } else { shouldCleanLog = j.updateInboundClientIps(inboundClientIps, clientEmail, ips) } } // added delay before cleaning logs to reduce chance of logging IP that already has been banned time.Sleep(time.Second * 2) if shouldCleanLog { j.clearAccessLog() } } func (j *CheckClientIpJob) checkError(e error) { if e != nil { logger.Warning("client ip job err:", e) } } func (j *CheckClientIpJob) contains(s []string, str string) bool { for _, v := range s { if v == str { return true } } return false } 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 if err != nil { return nil, err } return InboundClientIps, nil } func (j *CheckClientIpJob) addInboundClientIps(clientEmail string, ips []string) error { inboundClientIps := &model.InboundClientIps{} jsonIps, err := json.Marshal(ips) j.checkError(err) inboundClientIps.ClientEmail = clientEmail inboundClientIps.Ips = string(jsonIps) db := database.GetDB() tx := db.Begin() defer func() { if err == nil { tx.Commit() } else { tx.Rollback() } }() err = tx.Save(inboundClientIps).Error if err != nil { return err } return nil } func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) bool { jsonIps, err := json.Marshal(ips) j.checkError(err) inboundClientIps.ClientEmail = clientEmail inboundClientIps.Ips = string(jsonIps) // check inbound limitation inbound, err := j.getInboundByEmail(clientEmail) j.checkError(err) if inbound.Settings == "" { logger.Debug("wrong data ", inbound) return false } settings := map[string][]model.Client{} json.Unmarshal([]byte(inbound.Settings), &settings) clients := settings["clients"] shouldCleanLog := false j.disAllowedIps = []string{} // 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 { j.disAllowedIps = append(j.disAllowedIps, ips[limitIp:]...) for i := limitIp; i < len(ips); i++ { log.Printf("[LIMIT_IP] Email = %s || SRC = %s", clientEmail, ips[i]) } } } } } sort.Strings(j.disAllowedIps) if len(j.disAllowedIps) > 0 { logger.Debug("disAllowedIps ", j.disAllowedIps) } db := database.GetDB() err = db.Save(inboundClientIps).Error if err != nil { return shouldCleanLog } return shouldCleanLog } 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 }