| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 | 
							- package job
 
- import (
 
- 	"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
 
- }
 
 
  |