| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 | package jobimport (	"bufio"	"encoding/json"	"io"	"log"	"os"	"os/exec"	"regexp"	"runtime"	"sort"	"time"	"github.com/mhsanaei/3x-ui/v2/database"	"github.com/mhsanaei/3x-ui/v2/database/model"	"github.com/mhsanaei/3x-ui/v2/logger"	"github.com/mhsanaei/3x-ui/v2/xray")// CheckClientIpJob monitors client IP addresses from access logs and manages IP blocking based on configured limits.type CheckClientIpJob struct {	lastClear     int64	disAllowedIps []string}var job *CheckClientIpJob// NewCheckClientIpJob creates a new client IP monitoring job instance.func 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 isAccessLogAvailable {		if runtime.GOOS == "windows" {			if iplimitActive {				shouldClearAccessLog = j.processLogFile()			}		} else {			if iplimitActive {				if f2bInstalled {					shouldClearAccessLog = j.processLogFile()				} else {					if !f2bInstalled {						logger.Warning("[LimitIP] Fail2Ban is not installed, Please install Fail2Ban from the x-ui bash menu.")					}				}			}		}	}	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)	defer logAccessP.Close()	accessLogPath, err := xray.GetAccessLogPath()	j.checkError(err)	file, err := os.Open(accessLogPath)	j.checkError(err)	defer file.Close()	_, err = io.Copy(logAccessP, file)	j.checkError(err)	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 {	ipRegex := regexp.MustCompile(`from (?:tcp:|udp:)?\[?([0-9a-fA-F\.:]+)\]?:\d+ accepted`)	emailRegex := regexp.MustCompile(`email: (.+)$`)	accessLogPath, _ := xray.GetAccessLogPath()	file, _ := os.Open(accessLogPath)	defer file.Close()	inboundClientIps := make(map[string]map[string]struct{}, 100)	scanner := bufio.NewScanner(file)	for scanner.Scan() {		line := scanner.Text()		ipMatches := ipRegex.FindStringSubmatch(line)		if len(ipMatches) < 2 {			continue		}		ip := ipMatches[1]		if ip == "127.0.0.1" || ip == "::1" {			continue		}		emailMatches := emailRegex.FindStringSubmatch(line)		if len(emailMatches) < 2 {			continue		}		email := emailMatches[1]		if _, exists := inboundClientIps[email]; !exists {			inboundClientIps[email] = make(map[string]struct{})		}		inboundClientIps[email][ip] = struct{}{}	}	shouldCleanLog := false	for email, uniqueIps := range inboundClientIps {		ips := make([]string, 0, len(uniqueIps))		for ip := range uniqueIps {			ips = append(ips, ip)		}		sort.Strings(ips)		clientIpsRecord, err := j.getInboundClientIps(email)		if err != nil {			j.addInboundClientIps(email, ips)			continue		}		shouldCleanLog = j.updateInboundClientIps(clientIpsRecord, email, ips) || shouldCleanLog	}	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("[LimitIP] Access log path is not set, Please configure the access log path in Xray configs.")		}		return false	}	return true}func (j *CheckClientIpJob) checkError(e error) {	if e != nil {		logger.Warning("client ip job err:", e)	}}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)	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	}	settings := map[string][]model.Client{}	json.Unmarshal([]byte(inbound.Settings), &settings)	clients := settings["clients"]	shouldCleanLog := false	j.disAllowedIps = []string{}	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)	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()	inbound := &model.Inbound{}	err := db.Model(&model.Inbound{}).Where("settings LIKE ?", "%"+clientEmail+"%").First(inbound).Error	if err != nil {		return nil, err	}	return inbound, nil}
 |