2023-02-28 22:54:29 +03:00
|
|
|
package job
|
|
|
|
|
|
|
|
import (
|
2024-02-03 13:41:57 +03:00
|
|
|
"bufio"
|
2023-04-13 22:37:13 +03:00
|
|
|
"encoding/json"
|
2024-02-03 13:41:57 +03:00
|
|
|
"io"
|
2023-06-24 23:36:18 +03:00
|
|
|
"log"
|
2023-04-13 22:37:13 +03:00
|
|
|
"os"
|
2023-09-01 12:53:50 +03:00
|
|
|
"os/exec"
|
2023-04-13 22:37:13 +03:00
|
|
|
"regexp"
|
2023-07-01 15:26:43 +03:00
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2023-02-28 22:54:29 +03:00
|
|
|
"x-ui/database"
|
|
|
|
"x-ui/database/model"
|
2023-04-13 22:37:13 +03:00
|
|
|
"x-ui/logger"
|
|
|
|
"x-ui/xray"
|
2023-02-28 22:54:29 +03:00
|
|
|
)
|
|
|
|
|
2024-01-21 14:09:15 +03:00
|
|
|
type CheckClientIpJob struct {
|
2024-03-05 16:39:20 +03:00
|
|
|
lastClear int64
|
2024-01-21 14:09:15 +03:00
|
|
|
disAllowedIps []string
|
|
|
|
}
|
2023-04-13 22:37:13 +03:00
|
|
|
|
2023-02-28 22:54:29 +03:00
|
|
|
var job *CheckClientIpJob
|
|
|
|
|
|
|
|
func NewCheckClientIpJob() *CheckClientIpJob {
|
2023-06-03 18:29:32 +03:00
|
|
|
job = new(CheckClientIpJob)
|
2023-02-28 22:54:29 +03:00
|
|
|
return job
|
|
|
|
}
|
|
|
|
|
|
|
|
func (j *CheckClientIpJob) Run() {
|
2024-03-05 16:39:20 +03:00
|
|
|
if j.lastClear == 0 {
|
|
|
|
j.lastClear = time.Now().Unix()
|
2023-07-01 15:26:43 +03:00
|
|
|
}
|
2023-06-24 23:36:18 +03:00
|
|
|
|
2024-03-11 00:31:24 +03:00
|
|
|
shouldClearAccessLog := false
|
2024-03-05 16:39:20 +03:00
|
|
|
f2bInstalled := j.checkFail2BanInstalled()
|
2024-03-11 00:31:24 +03:00
|
|
|
isAccessLogAvailable := j.checkAccessLogAvailable(f2bInstalled)
|
2024-03-05 16:39:20 +03:00
|
|
|
|
2023-07-01 15:26:43 +03:00
|
|
|
if j.hasLimitIp() {
|
2024-03-11 00:31:24 +03:00
|
|
|
if f2bInstalled && isAccessLogAvailable {
|
|
|
|
shouldClearAccessLog = j.processLogFile()
|
2024-03-02 20:40:12 +03:00
|
|
|
} else {
|
2024-03-05 16:39:20 +03:00
|
|
|
if !f2bInstalled {
|
2024-03-02 20:40:12 +03:00
|
|
|
logger.Warning("fail2ban is not installed. IP limiting may not work properly.")
|
|
|
|
}
|
|
|
|
}
|
2023-06-16 00:38:35 +03:00
|
|
|
}
|
2024-02-10 01:22:20 +03:00
|
|
|
|
2024-03-11 00:31:24 +03:00
|
|
|
if shouldClearAccessLog || isAccessLogAvailable && time.Now().Unix()-j.lastClear > 3600 {
|
2024-02-10 01:22:20 +03:00
|
|
|
j.clearAccessLog()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (j *CheckClientIpJob) clearAccessLog() {
|
2024-03-11 00:31:24 +03:00
|
|
|
logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
2024-02-10 13:40:39 +03:00
|
|
|
j.checkError(err)
|
|
|
|
|
|
|
|
// reopen the access log file for reading
|
2024-03-05 16:39:20 +03:00
|
|
|
accessLogPath := xray.GetAccessLogPath()
|
2024-02-10 13:40:39 +03:00
|
|
|
file, err := os.Open(accessLogPath)
|
|
|
|
j.checkError(err)
|
|
|
|
|
|
|
|
// copy access log content to persistent file
|
|
|
|
_, err = io.Copy(logAccessP, file)
|
|
|
|
j.checkError(err)
|
|
|
|
|
2024-03-02 20:40:12 +03:00
|
|
|
// close the file after copying content
|
2024-03-05 16:39:20 +03:00
|
|
|
logAccessP.Close()
|
2024-03-02 20:40:12 +03:00
|
|
|
file.Close()
|
|
|
|
|
2024-02-10 13:40:39 +03:00
|
|
|
// clean access log
|
|
|
|
err = os.Truncate(accessLogPath, 0)
|
2024-02-10 01:22:20 +03:00
|
|
|
j.checkError(err)
|
2024-03-05 16:39:20 +03:00
|
|
|
j.lastClear = time.Now().Unix()
|
2023-02-28 22:54:29 +03:00
|
|
|
}
|
|
|
|
|
2023-07-01 15:26:43 +03:00
|
|
|
func (j *CheckClientIpJob) hasLimitIp() bool {
|
2023-06-16 00:38:35 +03:00
|
|
|
db := database.GetDB()
|
|
|
|
var inbounds []*model.Inbound
|
2023-07-01 15:26:43 +03:00
|
|
|
|
2023-06-16 00:38:35 +03:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-07-01 15:26:43 +03:00
|
|
|
|
2023-06-16 00:38:35 +03:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2024-03-02 20:40:12 +03:00
|
|
|
func (j *CheckClientIpJob) checkFail2BanInstalled() bool {
|
2023-09-01 12:53:50 +03:00
|
|
|
cmd := "fail2ban-client"
|
|
|
|
args := []string{"-h"}
|
|
|
|
err := exec.Command(cmd, args...).Run()
|
2024-03-02 20:40:12 +03:00
|
|
|
return err == nil
|
|
|
|
}
|
|
|
|
|
2024-03-05 16:39:20 +03:00
|
|
|
func (j *CheckClientIpJob) processLogFile() bool {
|
2023-07-01 15:26:43 +03:00
|
|
|
accessLogPath := xray.GetAccessLogPath()
|
2024-02-03 17:24:04 +03:00
|
|
|
|
2024-02-03 13:41:57 +03:00
|
|
|
file, err := os.Open(accessLogPath)
|
2023-07-01 15:26:43 +03:00
|
|
|
j.checkError(err)
|
2024-02-03 13:41:57 +03:00
|
|
|
|
|
|
|
InboundClientIps := make(map[string][]string)
|
|
|
|
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
|
|
for scanner.Scan() {
|
|
|
|
line := scanner.Text()
|
2023-02-28 22:54:29 +03:00
|
|
|
|
2024-02-04 01:21:31 +03:00
|
|
|
ipRegx, _ := regexp.Compile(`(\d+\.\d+\.\d+\.\d+).* accepted`)
|
2023-06-03 18:29:32 +03:00
|
|
|
emailRegx, _ := regexp.Compile(`email:.+`)
|
2023-02-28 22:54:29 +03:00
|
|
|
|
2024-02-04 01:21:31 +03:00
|
|
|
matches := ipRegx.FindStringSubmatch(line)
|
|
|
|
if len(matches) > 1 {
|
|
|
|
ip := matches[1]
|
2024-02-10 01:22:20 +03:00
|
|
|
if ip == "127.0.0.1" {
|
2023-02-28 22:54:29 +03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
matchesEmail := emailRegx.FindString(line)
|
2023-04-13 22:37:13 +03:00
|
|
|
if matchesEmail == "" {
|
2023-02-28 22:54:29 +03:00
|
|
|
continue
|
|
|
|
}
|
2023-05-23 17:24:15 +03:00
|
|
|
matchesEmail = strings.TrimSpace(strings.Split(matchesEmail, "email: ")[1])
|
2023-04-13 22:37:13 +03:00
|
|
|
|
2023-06-03 18:29:32 +03:00
|
|
|
if InboundClientIps[matchesEmail] != nil {
|
2023-07-01 15:26:43 +03:00
|
|
|
if j.contains(InboundClientIps[matchesEmail], ip) {
|
2023-06-03 18:29:32 +03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
|
|
|
|
} else {
|
2023-04-13 22:37:13 +03:00
|
|
|
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
|
|
|
|
}
|
2023-02-28 22:54:29 +03:00
|
|
|
}
|
|
|
|
}
|
2023-07-01 15:26:43 +03:00
|
|
|
|
2024-02-03 13:41:57 +03:00
|
|
|
j.checkError(scanner.Err())
|
2024-03-05 16:39:20 +03:00
|
|
|
file.Close()
|
2024-02-03 13:41:57 +03:00
|
|
|
|
2023-06-15 22:15:34 +03:00
|
|
|
shouldCleanLog := false
|
2023-02-28 22:54:29 +03:00
|
|
|
|
|
|
|
for clientEmail, ips := range InboundClientIps {
|
2023-07-01 15:26:43 +03:00
|
|
|
inboundClientIps, err := j.getInboundClientIps(clientEmail)
|
2023-04-28 00:00:49 +03:00
|
|
|
sort.Strings(ips)
|
2023-04-13 22:37:13 +03:00
|
|
|
if err != nil {
|
2023-07-01 15:26:43 +03:00
|
|
|
j.addInboundClientIps(clientEmail, ips)
|
2023-04-13 22:37:13 +03:00
|
|
|
} else {
|
2023-07-01 15:26:43 +03:00
|
|
|
shouldCleanLog = j.updateInboundClientIps(inboundClientIps, clientEmail, ips)
|
2023-02-28 22:54:29 +03:00
|
|
|
}
|
2023-04-13 22:37:13 +03:00
|
|
|
}
|
2023-06-16 00:38:35 +03:00
|
|
|
|
2024-03-05 16:39:20 +03:00
|
|
|
return shouldCleanLog
|
2023-02-28 22:54:29 +03:00
|
|
|
}
|
2023-07-01 15:26:43 +03:00
|
|
|
|
2024-03-11 00:31:24 +03:00
|
|
|
func (j *CheckClientIpJob) checkAccessLogAvailable(doWarning bool) bool {
|
|
|
|
accessLogPath := xray.GetAccessLogPath()
|
|
|
|
isAvailable := true
|
|
|
|
warningMsg := ""
|
|
|
|
// access log is not available if it is set to 'none' or an empty string
|
|
|
|
switch accessLogPath {
|
|
|
|
case "none":
|
|
|
|
warningMsg = "Access log is set to 'none', check your Xray Configs"
|
|
|
|
isAvailable = false
|
|
|
|
case "":
|
|
|
|
warningMsg = "Access log doesn't exist in your Xray Configs"
|
|
|
|
isAvailable = false
|
|
|
|
}
|
|
|
|
if doWarning && warningMsg != "" {
|
|
|
|
logger.Warning(warningMsg)
|
|
|
|
}
|
|
|
|
return isAvailable
|
|
|
|
}
|
|
|
|
|
2023-07-01 15:26:43 +03:00
|
|
|
func (j *CheckClientIpJob) checkError(e error) {
|
2023-04-13 22:37:13 +03:00
|
|
|
if e != nil {
|
2023-02-28 22:54:29 +03:00
|
|
|
logger.Warning("client ip job err:", e)
|
|
|
|
}
|
|
|
|
}
|
2023-07-01 15:26:43 +03:00
|
|
|
|
|
|
|
func (j *CheckClientIpJob) contains(s []string, str string) bool {
|
2023-02-28 22:54:29 +03:00
|
|
|
for _, v := range s {
|
|
|
|
if v == str {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
2023-06-03 18:29:32 +03:00
|
|
|
|
2023-02-28 22:54:29 +03:00
|
|
|
return false
|
|
|
|
}
|
2023-07-01 15:26:43 +03:00
|
|
|
|
|
|
|
func (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
|
2023-02-28 22:54:29 +03:00
|
|
|
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
|
|
|
|
}
|
2023-07-01 15:26:43 +03:00
|
|
|
|
|
|
|
func (j *CheckClientIpJob) addInboundClientIps(clientEmail string, ips []string) error {
|
2023-02-28 22:54:29 +03:00
|
|
|
inboundClientIps := &model.InboundClientIps{}
|
2023-03-25 19:35:46 +03:00
|
|
|
jsonIps, err := json.Marshal(ips)
|
2023-07-01 15:26:43 +03:00
|
|
|
j.checkError(err)
|
2023-02-28 22:54:29 +03:00
|
|
|
|
|
|
|
inboundClientIps.ClientEmail = clientEmail
|
|
|
|
inboundClientIps.Ips = string(jsonIps)
|
|
|
|
|
|
|
|
db := database.GetDB()
|
|
|
|
tx := db.Begin()
|
|
|
|
|
|
|
|
defer func() {
|
2023-06-03 18:29:32 +03:00
|
|
|
if err == nil {
|
2023-05-25 02:51:31 +03:00
|
|
|
tx.Commit()
|
2023-06-03 18:29:32 +03:00
|
|
|
} else {
|
|
|
|
tx.Rollback()
|
2023-02-28 22:54:29 +03:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
err = tx.Save(inboundClientIps).Error
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2023-06-03 18:29:32 +03:00
|
|
|
|
2023-07-01 15:26:43 +03:00
|
|
|
func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) bool {
|
2023-06-03 18:29:32 +03:00
|
|
|
jsonIps, err := json.Marshal(ips)
|
2023-07-01 15:26:43 +03:00
|
|
|
j.checkError(err)
|
2023-06-03 18:29:32 +03:00
|
|
|
|
|
|
|
inboundClientIps.ClientEmail = clientEmail
|
|
|
|
inboundClientIps.Ips = string(jsonIps)
|
2023-04-13 22:37:13 +03:00
|
|
|
|
2023-02-28 22:54:29 +03:00
|
|
|
// check inbound limitation
|
2023-07-01 15:26:43 +03:00
|
|
|
inbound, err := j.getInboundByEmail(clientEmail)
|
|
|
|
j.checkError(err)
|
2023-02-28 22:54:29 +03:00
|
|
|
|
|
|
|
if inbound.Settings == "" {
|
2023-04-13 22:37:13 +03:00
|
|
|
logger.Debug("wrong data ", inbound)
|
2023-06-08 13:20:35 +03:00
|
|
|
return false
|
2023-02-28 22:54:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
settings := map[string][]model.Client{}
|
|
|
|
json.Unmarshal([]byte(inbound.Settings), &settings)
|
|
|
|
clients := settings["clients"]
|
2023-06-15 22:15:34 +03:00
|
|
|
shouldCleanLog := false
|
2024-01-21 14:09:15 +03:00
|
|
|
j.disAllowedIps = []string{}
|
2023-02-28 22:54:29 +03:00
|
|
|
|
2023-07-01 15:26:43 +03:00
|
|
|
// create iplimit log file channel
|
2024-03-11 00:31:24 +03:00
|
|
|
logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
2023-07-01 15:26:43 +03:00
|
|
|
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)
|
|
|
|
|
2023-02-28 22:54:29 +03:00
|
|
|
for _, client := range clients {
|
|
|
|
if client.Email == clientEmail {
|
|
|
|
limitIp := client.LimitIP
|
2023-06-03 18:29:32 +03:00
|
|
|
|
2023-06-15 22:15:34 +03:00
|
|
|
if limitIp != 0 {
|
|
|
|
shouldCleanLog = true
|
|
|
|
|
|
|
|
if limitIp < len(ips) && inbound.Enable {
|
2024-01-21 14:09:15 +03:00
|
|
|
j.disAllowedIps = append(j.disAllowedIps, ips[limitIp:]...)
|
2023-06-16 00:38:35 +03:00
|
|
|
for i := limitIp; i < len(ips); i++ {
|
2023-06-24 23:36:18 +03:00
|
|
|
log.Printf("[LIMIT_IP] Email = %s || SRC = %s", clientEmail, ips[i])
|
2023-06-15 22:15:34 +03:00
|
|
|
}
|
|
|
|
}
|
2023-06-16 00:38:35 +03:00
|
|
|
}
|
2023-02-28 22:54:29 +03:00
|
|
|
}
|
|
|
|
}
|
2024-01-21 14:09:15 +03:00
|
|
|
|
|
|
|
sort.Strings(j.disAllowedIps)
|
|
|
|
|
|
|
|
if len(j.disAllowedIps) > 0 {
|
|
|
|
logger.Debug("disAllowedIps ", j.disAllowedIps)
|
|
|
|
}
|
2023-02-28 22:54:29 +03:00
|
|
|
|
|
|
|
db := database.GetDB()
|
|
|
|
err = db.Save(inboundClientIps).Error
|
2024-03-05 16:39:20 +03:00
|
|
|
j.checkError(err)
|
|
|
|
|
2023-06-15 22:15:34 +03:00
|
|
|
return shouldCleanLog
|
2023-02-28 22:54:29 +03:00
|
|
|
}
|
2023-06-08 13:20:35 +03:00
|
|
|
|
2023-07-01 15:26:43 +03:00
|
|
|
func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) {
|
2023-02-28 22:54:29 +03:00
|
|
|
db := database.GetDB()
|
|
|
|
var inbounds *model.Inbound
|
2023-07-01 15:26:43 +03:00
|
|
|
|
2023-04-13 22:37:13 +03:00
|
|
|
err := db.Model(model.Inbound{}).Where("settings LIKE ?", "%"+clientEmail+"%").Find(&inbounds).Error
|
2023-02-28 22:54:29 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-07-01 15:26:43 +03:00
|
|
|
|
2023-02-28 22:54:29 +03:00
|
|
|
return inbounds, nil
|
2023-06-16 00:38:35 +03:00
|
|
|
}
|