| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 | package jobimport (	"bufio"	"encoding/json"	"io"	"log"	"os"	"os/exec"	"regexp"	"sort"	"strings"	"time"	"x-ui/database"	"x-ui/database/model"	"x-ui/logger"	"x-ui/xray")type CheckClientIpJob struct {	lastClear     int64	disAllowedIps []string}var job *CheckClientIpJobfunc NewCheckClientIpJob() *CheckClientIpJob {	job = new(CheckClientIpJob)	return job}func (j *CheckClientIpJob) Run() {	if j.lastClear == 0 {		j.lastClear = time.Now().Unix()	}	shouldClearAccessLog := false	iplimitActive := j.hasLimitIp()	f2bInstalled := j.checkFail2BanInstalled()	isAccessLogAvailable := j.checkAccessLogAvailable(iplimitActive)	if iplimitActive {		if f2bInstalled && isAccessLogAvailable {			shouldClearAccessLog = j.processLogFile()		} else {			if !f2bInstalled {				logger.Warning("[iplimit] fail2ban is not installed. IP limiting may not work properly.")			}		}	}	if shouldClearAccessLog || (isAccessLogAvailable && time.Now().Unix()-j.lastClear > 3600) {		j.clearAccessLog()	}}func (j *CheckClientIpJob) clearAccessLog() {	logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)	j.checkError(err)	// get access log path to open it	accessLogPath, err := xray.GetAccessLogPath()	j.checkError(err)	// 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	logAccessP.Close()	file.Close()	// clean access log	err = os.Truncate(accessLogPath, 0)	j.checkError(err)	j.lastClear = time.Now().Unix()}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) processLogFile() bool {	accessLogPath, err := xray.GetAccessLogPath()	j.checkError(err)	file, err := os.Open(accessLogPath)	j.checkError(err)	InboundClientIps := make(map[string][]string)	scanner := bufio.NewScanner(file)	for scanner.Scan() {		line := scanner.Text()		ipRegx, _ := regexp.Compile(`from \[?([0-9a-fA-F:.]+)\]?:\d+ accepted`)		emailRegx, _ := regexp.Compile(`email: (\S+)$`)		matches := ipRegx.FindStringSubmatch(line)		if len(matches) > 1 {			ip := matches[1]			if ip == "127.0.0.1" || ip == "::1" {				continue			}			matchesEmail := emailRegx.FindString(line)			if matchesEmail == "" {				continue			}			matchesEmail = 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())	file.Close()	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)		}	}	return shouldCleanLog}func (j *CheckClientIpJob) checkFail2BanInstalled() bool {	cmd := "fail2ban-client"	args := []string{"-h"}	err := exec.Command(cmd, args...).Run()	return err == nil}func (j *CheckClientIpJob) checkAccessLogAvailable(iplimitActive bool) bool {	accessLogPath, err := xray.GetAccessLogPath()	if err != nil {		return false	}	if accessLogPath == "none" || accessLogPath == "" {		if iplimitActive {			logger.Warning("Access log path is not set, and IP limit is active. Please configure the access log path.")		}		return false	}	return true}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)	if err != nil {		logger.Error("failed to marshal IPs to JSON:", err)		return false	}	inboundClientIps.ClientEmail = clientEmail	inboundClientIps.Ips = string(jsonIps)	// Fetch inbound settings by client email	inbound, err := j.getInboundByEmail(clientEmail)	if err != nil {		logger.Errorf("failed to fetch inbound settings for email %s: %s", clientEmail, err)		return false	}	if inbound.Settings == "" {		logger.Debug("wrong data:", inbound)		return false	}	// Unmarshal settings to get client limits	settings := map[string][]model.Client{}	json.Unmarshal([]byte(inbound.Settings), &settings)	clients := settings["clients"]	shouldCleanLog := false	j.disAllowedIps = []string{}	// Open log file for IP limits	logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)	if err != nil {		logger.Errorf("failed to open IP limit log file: %s", err)		return false	}	defer logIpFile.Close()	log.SetOutput(logIpFile)	log.SetFlags(log.LstdFlags)	// Check client IP limits	for _, client := range clients {		if client.Email == clientEmail {			limitIp := client.LimitIP			if limitIp > 0 && inbound.Enable {				shouldCleanLog = true				if limitIp < len(ips) {					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 {		logger.Error("failed to save inboundClientIps:", err)		return false	}	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}
 |