Merge branch 'main' into main

This commit is contained in:
Hossin Asaadi
2022-11-08 17:48:42 +03:30
committed by GitHub
19 changed files with 634 additions and 83 deletions

View File

@@ -1157,16 +1157,22 @@ Inbound.VmessSettings = class extends Inbound.Settings {
}
};
Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), alterId=0) {
constructor(id=RandomUtil.randomUUID(), alterId=0, email='', limitIp=0) {
super();
this.id = id;
this.alterId = alterId;
this.email = email;
this.limitIp = limitIp;
}
static fromJson(json={}) {
return new Inbound.VmessSettings.Vmess(
json.id,
json.alterId,
json.email,
json.limitIp,
);
}
};
@@ -1209,16 +1215,20 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
};
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), flow=FLOW_CONTROL.DIRECT) {
constructor(id=RandomUtil.randomUUID(), flow=FLOW_CONTROL.DIRECT, email='', limitIp=0) {
super();
this.id = id;
this.flow = flow;
this.email = email;
this.limitIp = limitIp;
}
static fromJson(json={}) {
return new Inbound.VLESSSettings.VLESS(
json.id,
json.flow,
json.email,
json.limitIp
);
}
};

View File

@@ -30,6 +30,11 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/add", a.addInbound)
g.POST("/del/:id", a.delInbound)
g.POST("/update/:id", a.updateInbound)
g.POST("/clientIps/:email", a.getClientIps)
g.POST("/clearClientIps/:email", a.clearClientIps)
}
func (a *InboundController) startTask() {
@@ -106,3 +111,23 @@ func (a *InboundController) updateInbound(c *gin.Context) {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) getClientIps(c *gin.Context) {
email := c.Param("email")
ips , err := a.inboundService.GetInboundClientIps(email)
if err != nil {
jsonObj(c, "No IP Record", nil)
return
}
jsonObj(c, ips, nil)
}
func (a *InboundController) clearClientIps(c *gin.Context) {
email := c.Param("email")
err := a.inboundService.ClearClientIps(email)
if err != nil {
jsonMsg(c, "修改", err)
return
}
jsonMsg(c, "Log Cleared", nil)
}

View File

@@ -1,5 +1,39 @@
{{define "form/vless"}}
<a-form layout="inline">
<a-form layout="inline">
<a-form-item label="Email">
<a-input v-model.trim="inbound.settings.vlesses[0].email"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
IP Count Limit
<a-tooltip>
<template slot="title">
disable inbound if more than entered count (0 for disable limit ip)
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input type="number" v-model.number="inbound.settings.vlesses[0].limitIp"></a-input>
</a-form-item>
<a-form-item v-if="inbound.settings.vlesses[0].email && inbound.settings.vlesses[0].limitIp > 0 && isEdit">
<span slot="label">
client IP log
<a-tooltip>
<template slot="title">
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-textarea disabled :input="getDBClientIps(inbound.settings.vlesses[0].email)" v-model="clientIps" :auto-size="{ minRows: 3, maxRows: 3 }">
</a-textarea>
<a-button type="danger" @click="clearDBClientIps(inbound.settings.vlesses[0].email)" >clear log</a-button>
</a-form-item>
</a-form>
<a-form-item label="id">
<a-input v-model.trim="inbound.settings.vlesses[0].id"></a-input>
</a-form-item>

View File

@@ -1,5 +1,40 @@
{{define "form/vmess"}}
<a-form layout="inline">
<a-form layout="inline">
<a-form-item label="Email">
<a-input v-model.trim="inbound.settings.vmesses[0].email"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
IP Count Limit
<a-tooltip>
<template slot="title">
disable inbound if more than entered count (0 for disable limit ip)
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input type="number" v-model.number="inbound.settings.vmesses[0].limitIp"></a-input>
</a-form-item>
<a-form-item v-if="inbound.settings.vmesses[0].email && inbound.settings.vmesses[0].limitIp > 0 && isEdit">
<span slot="label">
Client IP Log
<a-tooltip>
<template slot="title">
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-textarea disabled :input="getDBClientIps(inbound.settings.vmesses[0].email)" v-model="clientIps" :auto-size="{ minRows: 3, maxRows: 3 }">
</a-textarea>
<a-button type="danger" @click="clearDBClientIps(inbound.settings.vmesses[0].email)" >clear log</a-button>
</a-form-item>
</a-form>
<a-form-item label="id">
<a-input v-model.trim="inbound.settings.vmesses[0].id"></a-input>
</a-form-item>

View File

@@ -11,13 +11,15 @@
visible: false,
confirmLoading: false,
okText: '{{ i18n "sure" }}',
isEdit: false,
confirm: null,
inbound: new Inbound(),
dbInbound: new DBInbound(),
clientIps: "",
ok() {
ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
},
show({ title='', okText='{{ i18n "sure" }}', inbound=null, dbInbound=null, confirm=(inbound, dbInbound)=>{} }) {
show({ title='', okText='{{ i18n "sure" }}', inbound=null, dbInbound=null, confirm=(inbound, dbInbound)=>{}, isEdit=false }) {
this.title = title;
this.okText = okText;
if (inbound) {
@@ -32,6 +34,7 @@
}
this.confirm = confirm;
this.visible = true;
this.isEdit = isEdit;
},
close() {
inModal.visible = false;
@@ -64,6 +67,12 @@
},
get dbInbound() {
return inModal.dbInbound;
},
get clientIps() {
return inModal.clientIps;
},
get isEdit() {
return inModal.isEdit;
}
},
methods: {
@@ -71,8 +80,34 @@
if (oldValue === 'kcp') {
this.inModal.inbound.tls = false;
}
}
}
},
async getDBClientIps(email) {
const msg = await HttpUtil.post('/xui/inbound/clientIps/'+ email);
if (!msg.success) {
return;
}
try {
ips = JSON.parse(msg.obj)
ips = ips.join(",")
this.inModal.clientIps = ips
} catch (error) {
// text
this.inModal.clientIps = msg.obj
}
},
async clearDBClientIps(email) {
const msg = await HttpUtil.post('/xui/inbound/clearClientIps/'+ email);
if (!msg.success) {
return;
}
this.inModal.clientIps = ""
},
},
});
</script>

View File

@@ -243,7 +243,8 @@
inModal.loading();
await this.addInbound(inbound, dbInbound);
inModal.close();
}
},
isEdit: false
});
},
openEditInbound(dbInbound) {
@@ -258,7 +259,8 @@
inModal.loading();
await this.updateInbound(inbound, dbInbound);
inModal.close();
}
},
isEdit: true
});
},
async addInbound(inbound, dbInbound) {

View File

@@ -0,0 +1,350 @@
package job
import (
"x-ui/logger"
"x-ui/web/service"
"x-ui/database"
"x-ui/database/model"
"os"
ss "strings"
"regexp"
"encoding/json"
"strconv"
"strings"
"time"
"net"
"github.com/go-cmd/cmd"
"sort"
)
type CheckClientIpJob struct {
xrayService service.XrayService
inboundService service.InboundService
}
var job *CheckClientIpJob
var disAllowedIps []string
func NewCheckClientIpJob() *CheckClientIpJob {
job = new(CheckClientIpJob)
return job
}
func (j *CheckClientIpJob) Run() {
logger.Debug("Check Client IP Job...")
processLogFile()
// disAllowedIps = []string{"192.168.1.183","192.168.1.197"}
blockedIps := []byte(ss.Join(disAllowedIps,","))
err := os.WriteFile("./bin/blockedIPs", blockedIps, 0755)
checkError(err)
}
func processLogFile() {
accessLogPath := GetAccessLogPath()
if(accessLogPath == "") {
logger.Warning("xray log not init in config.json")
return
}
data, err := os.ReadFile(accessLogPath)
InboundClientIps := make(map[string][]string)
checkError(err)
// clean log
if err := os.Truncate(GetAccessLogPath(), 0); err != nil {
checkError(err)
}
lines := ss.Split(string(data), "\n")
for _, line := range lines {
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
emailRegx, _ := regexp.Compile(`email:.+`)
matchesIp := ipRegx.FindString(line)
if(len(matchesIp) > 0) {
ip := string(matchesIp)
if( ip == "127.0.0.1" || ip == "1.1.1.1") {
continue
}
matchesEmail := emailRegx.FindString(line)
if(matchesEmail == "") {
continue
}
matchesEmail = ss.Split(matchesEmail, "email: ")[1]
if(InboundClientIps[matchesEmail] != nil) {
if(contains(InboundClientIps[matchesEmail],ip)){
continue
}
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail],ip)
}else{
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail],ip)
}
}
}
disAllowedIps = []string{}
for clientEmail, ips := range InboundClientIps {
inboundClientIps,err := GetInboundClientIps(clientEmail)
sort.Sort(sort.StringSlice(ips))
if(err != nil){
addInboundClientIps(clientEmail,ips)
}else{
updateInboundClientIps(inboundClientIps,clientEmail,ips)
}
}
// check if inbound connection is more than limited ip and drop connection
LimitDevice := func() { LimitDevice() }
stop := schedule(LimitDevice, 1000 *time.Millisecond)
time.Sleep(10 * time.Second)
stop <- true
}
func GetAccessLogPath() string {
config, err := os.ReadFile("bin/config.json")
checkError(err)
jsonConfig := map[string]interface{}{}
err = json.Unmarshal([]byte(config), &jsonConfig)
checkError(err)
if(jsonConfig["log"] != nil) {
jsonLog := jsonConfig["log"].(map[string]interface{})
if(jsonLog["access"] != nil) {
accessLogPath := jsonLog["access"].(string)
return accessLogPath
}
}
return ""
}
func checkError(e error) {
if e != nil {
logger.Warning("client ip job err:", e)
}
}
func contains(s []string, str string) bool {
for _, v := range s {
if v == str {
return true
}
}
return false
}
func 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 addInboundClientIps(clientEmail string,ips []string) error {
inboundClientIps := &model.InboundClientIps{}
jsonIps, err := json.Marshal(ips)
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 updateInboundClientIps(inboundClientIps *model.InboundClientIps,clientEmail string,ips []string) error {
jsonIps, err := json.Marshal(ips)
checkError(err)
inboundClientIps.ClientEmail = clientEmail
inboundClientIps.Ips = string(jsonIps)
// check inbound limitation
inbound, err := GetInboundByEmail(clientEmail)
checkError(err)
limitIpRegx, _ := regexp.Compile(`"limitIp": .+`)
if inbound.Settings == "" {
logger.Debug("wrong data ",inbound)
return nil
}
limitIpMactch := limitIpRegx.FindString(inbound.Settings)
limitIpMactch = ss.Split(limitIpMactch, `"limitIp": `)[1]
limitIp, err := strconv.Atoi(limitIpMactch)
if(limitIp < len(ips) && limitIp != 0 && inbound.Enable) {
if(limitIp == 1){
limitIp = 2
}
disAllowedIps = append(disAllowedIps,ips[limitIp - 1:]...)
}
logger.Debug("disAllowedIps ",disAllowedIps)
sort.Sort(sort.StringSlice(disAllowedIps))
db := database.GetDB()
err = db.Save(inboundClientIps).Error
if err != nil {
return err
}
return nil
}
func DisableInbound(id int) error{
db := database.GetDB()
result := db.Model(model.Inbound{}).
Where("id = ? and enable = ?", id, true).
Update("enable", false)
err := result.Error
logger.Warning("disable inbound with id:",id)
if err == nil {
job.xrayService.SetToNeedRestart()
}
return err
}
func 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
}
func LimitDevice(){
localIp,err := LocalIP()
checkError(err)
c := cmd.NewCmd("bash","-c","ss --tcp | grep -E '" + IPsToRegex(localIp) + "'| awk '{if($1==\"ESTAB\") print $4,$5;}'","| sort | uniq -c | sort -nr | head")
<-c.Start()
if len(c.Status().Stdout) > 0 {
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
portRegx, _ := regexp.Compile(`(?:(:))([0-9]..[^.][0-9]+)`)
for _, row := range c.Status().Stdout {
data := strings.Split(row," ")
destIp,destPort,srcIp,srcPort := "","","",""
destIp = string(ipRegx.FindString(data[0]))
destPort = portRegx.FindString(data[0])
destPort = strings.Replace(destPort,":","",-1)
srcIp = string(ipRegx.FindString(data[1]))
srcPort = portRegx.FindString(data[1])
srcPort = strings.Replace(srcPort,":","",-1)
if(contains(disAllowedIps,srcIp)){
dropCmd := cmd.NewCmd("bash","-c","ss -K dport = " + srcPort)
dropCmd.Start()
logger.Debug("request droped : ",srcIp,srcPort,"to",destIp,destPort)
}
}
}
}
func LocalIP() ([]string, error) {
// get machine ips
ifaces, err := net.Interfaces()
ips := []string{}
if err != nil {
return ips, err
}
for _, i := range ifaces {
addrs, err := i.Addrs()
if err != nil {
return ips, err
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
ips = append(ips,ip.String())
}
}
logger.Debug("System IPs : ",ips)
return ips, nil
}
func IPsToRegex(ips []string) (string){
regx := ""
for _, ip := range ips {
regx += "(" + strings.Replace(ip, ".", "\\.", -1) + ")"
}
regx = "(" + strings.Replace(regx, ")(", ")|(.", -1) + ")"
return regx
}
func schedule(LimitDevice func(), delay time.Duration) chan bool {
stop := make(chan bool)
go func() {
for {
LimitDevice()
select {
case <-time.After(delay):
case <-stop:
return
}
}
}()
return stop
}

View File

@@ -176,3 +176,27 @@ func (s *InboundService) DisableInvalidInbounds() (int64, error) {
count := result.RowsAffected
return count, err
}
func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) {
db := database.GetDB()
InboundClientIps := &model.InboundClientIps{}
err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error
if err != nil {
return "", err
}
return InboundClientIps.Ips, nil
}
func (s *InboundService) ClearClientIps(clientEmail string) (error) {
db := database.GetDB()
result := db.Model(model.InboundClientIps{}).
Where("client_email = ?", clientEmail).
Update("ips", "")
err := result.Error
if err != nil {
return err
}
return nil
}

View File

@@ -307,6 +307,10 @@ func (s *Server) startTask() {
// 每 30 秒检查一次 inbound 流量超出和到期的情况
s.cron.AddJob("@every 30s", job.NewCheckInboundJob())
// check client ips from log file every 10 sec
s.cron.AddJob("@every 10s", job.NewCheckClientIpJob())
// 每一天提示一次流量情况,上海时间8点30
var entry cron.EntryID
isTgbotenabled, err := s.settingService.GetTgbotenabled()