Răsfoiți Sursa

ip limit + export links

MHSanaei 2 ani în urmă
părinte
comite
5317df21f3

+ 2 - 1
.github/workflows/release.yml

@@ -22,7 +22,7 @@ jobs:
           mv xui-release x-ui
           mkdir bin
           cd bin
-          wget https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-64.zip
+          wget https://github.com/mhsanaei/Xray-core/releases/latest/download/Xray-linux-64.zip
           unzip Xray-linux-64.zip
           rm -f Xray-linux-64.zip geoip.dat geosite.dat
           wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
@@ -40,3 +40,4 @@ jobs:
           file: x-ui-linux-amd64.tar.gz
           asset_name: x-ui-linux-amd64.tar.gz
           prerelease: true
+          overwrite: true

+ 7 - 0
database/db.go

@@ -42,6 +42,9 @@ func initInbound() error {
 func initSetting() error {
 	return db.AutoMigrate(&model.Setting{})
 }
+func initInboundClientIps() error {
+	return db.AutoMigrate(&model.InboundClientIps{})
+}
 func initClientTraffic() error {
 	return db.AutoMigrate(&xray.ClientTraffic{})
 }
@@ -81,6 +84,10 @@ func InitDB(dbPath string) error {
 	if err != nil {
 		return err
 	}
+	err = initInboundClientIps()
+	if err != nil {
+		return err
+	}
 	err = initClientTraffic()
 	if err != nil {
 		return err

+ 6 - 0
database/model/model.go

@@ -43,6 +43,11 @@ type Inbound struct {
 	Tag            string   `json:"tag" form:"tag" gorm:"unique"`
 	Sniffing       string   `json:"sniffing" form:"sniffing"`
 }
+type InboundClientIps struct {
+	Id       int    `json:"id" gorm:"primaryKey;autoIncrement"`
+	ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
+	Ips string `json:"ips" form:"ips"`
+}
 
 func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
 	listen := i.Listen
@@ -70,6 +75,7 @@ type Client struct {
 	ID         string `json:"id"`
 	AlterIds   uint16 `json:"alterId"`
 	Email      string `json:"email"`
+	LimitIP    int    `json:"limitIp"`
 	Security   string `json:"security"`
 	TotalGB    int64  `json:"totalGB" form:"totalGB"`
 	ExpiryTime int64  `json:"expiryTime" form:"expiryTime"`

+ 1 - 0
go.mod

@@ -6,6 +6,7 @@ require (
 	github.com/Workiva/go-datastructures v1.0.53
 	github.com/gin-contrib/sessions v0.0.4
 	github.com/gin-gonic/gin v1.9.0
+	github.com/go-cmd/cmd v1.4.1
 	github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
 	github.com/nicksnyder/go-i18n/v2 v2.2.1
 	github.com/op/go-logging v0.0.0-20160315200505-970db520ece7

+ 3 - 0
go.sum

@@ -28,6 +28,8 @@ github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjX
 github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
 github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
 github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
+github.com/go-cmd/cmd v1.4.1 h1:JUcEIE84v8DSy02XTZpUDeGKExk2oW3DA10hTjbQwmc=
+github.com/go-cmd/cmd v1.4.1/go.mod h1:tbBenttXtZU4c5djS1o7PWL5pd2xAr5sIqH1kGdNiRc=
 github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
 github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@@ -44,6 +46,7 @@ github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVL
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
+github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
 github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
 github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=

+ 10 - 2
web/assets/js/model/models.js

@@ -36,7 +36,7 @@ class DBInbound {
         this.remark = "";
         this.enable = true;
         this.expiryTime = 0;
-
+        this.iplimit = 0;
         this.listen = "";
         this.port = 0;
         this.protocol = "";
@@ -109,6 +109,10 @@ class DBInbound {
     get isExpiry() {
         return this.expiryTime < new Date().getTime();
     }
+	get isDBInboundEmpty() {
+        const inbound = this.toInbound();
+        return inbound.isInboundEmpty();
+    }
 
     toInbound() {
         let settings = {};
@@ -151,10 +155,14 @@ class DBInbound {
         }
     }
 
-    genLink(clientIndex) {
+    genLink(clientIndex = 0) {
         const inbound = this.toInbound();
         return inbound.genLink(this.address, this.remark, clientIndex);
     }
+	get genInboundLinks() {
+        const inbound = this.toInbound();
+        return inbound.genInboundLinks(this.address, this.remark);
+    }
 }
 
 class AllSetting {

+ 32 - 6
web/assets/js/model/xray.js

@@ -101,6 +101,7 @@ Object.freeze(XTLS_FLOW_CONTROL);
 Object.freeze(TLS_FLOW_CONTROL);
 Object.freeze(TLS_VERSION_OPTION);
 Object.freeze(TLS_CIPHER_OPTION);
+Object.freeze(UTLS_FINGERPRINT);
 
 class XrayCommonClass {
 
@@ -1065,7 +1066,6 @@ class Inbound extends XrayCommonClass {
         params.set("type", this.stream.network);
         if (this.xtls) {
             params.set("security", "xtls");
-            address = this.stream.tls.server;
         } else {
             params.set("security", this.stream.security);
         }
@@ -1119,7 +1119,10 @@ class Inbound extends XrayCommonClass {
                 address = this.stream.tls.server;
                 params.set("sni", address);
             }
-			params.set("flow", this.settings.vlesses[clientIndex].flow);
+			if (this.settings.vlesses[clientIndex].flow === "xtls-rprx-vision") {
+                params.set("flow", this.settings.vlesses[clientIndex].flow);
+            }
+            params.set("fp", this.settings.vlesses[clientIndex].fingerprint);
         }
 
         if (this.xtls) {
@@ -1135,7 +1138,7 @@ class Inbound extends XrayCommonClass {
         return url.toString();
     }
 
-    genSSLink(address = '', remark = '') {
+    genSSLink(address = '', remark = '',clientIndex) {
         let settings = this.settings;
         const server = this.stream.tls.server;
         if (!ObjectUtil.isEmpty(server)) {
@@ -1245,6 +1248,22 @@ class Inbound extends XrayCommonClass {
             default: return '';
         }
     }
+    genInboundLinks(address = '', remark = '') {
+    let link = '';
+    JSON.parse(this.settings)
+    switch (this.protocol) {
+        case Protocols.VMESS:
+        case Protocols.VLESS:
+        case Protocols.TROJAN:
+            JSON.parse(this.settings).clients.forEach((client,index) => {
+                link += this.genLink(address, remark, index) + '\r\n';
+            });
+            return link;
+        case Protocols.SHADOWSOCKS:
+            return (this.genSSLink(address, remark) + '\r\n');
+        default: return '';
+    }
+}
 
     static fromJson(json={}) {
         return new Inbound(
@@ -1359,11 +1378,12 @@ Inbound.VmessSettings = class extends Inbound.Settings {
     }
 };
 Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
-    constructor(id=RandomUtil.randomUUID(), alterId=0, email=RandomUtil.randomText(), totalGB=0, expiryTime='') {
+    constructor(id=RandomUtil.randomUUID(), alterId=0, email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime='') {
         super();
         this.id = id;
         this.alterId = alterId;
         this.email = email;
+        this.limitIp = limitIp;
         this.totalGB = totalGB;
         this.expiryTime = expiryTime;
     }
@@ -1373,6 +1393,7 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
             json.id,
             json.alterId,
             json.email,
+            json.limitIp,
             json.totalGB,
             json.expiryTime,
 
@@ -1441,11 +1462,12 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
 };
 Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
 
-    constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomText(), totalGB=0, fingerprint = UTLS_FINGERPRINT.UTLS_CHROME, expiryTime='') {
+    constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomText(),limitIp=0, totalGB=0, fingerprint = UTLS_FINGERPRINT.UTLS_CHROME, expiryTime='') {
         super();
         this.id = id;
         this.flow = flow;
         this.email = email;
+        this.limitIp = limitIp;
         this.totalGB = totalGB;
         this.fingerprint = fingerprint;
         this.expiryTime = expiryTime;
@@ -1457,6 +1479,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
             json.id,
             json.flow,
             json.email,
+            json.limitIp,
             json.totalGB,
             json.fingerprint,
             json.expiryTime,
@@ -1557,11 +1580,12 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
     }
 };
 Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
-    constructor(password=RandomUtil.randomSeq(10), flow ='', email=RandomUtil.randomText(), totalGB=0, expiryTime='') {
+    constructor(password=RandomUtil.randomSeq(10), flow ='', email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime='') {
         super();
         this.password = password;
         this.flow = flow;
         this.email = email;
+        this.limitIp = limitIp;
         this.totalGB = totalGB;
         this.expiryTime = expiryTime;
     }
@@ -1571,6 +1595,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
             password: this.password,
             flow: this.flow,
             email: this.email,
+            limitIp: this.limitIp,
             totalGB: this.totalGB,
             expiryTime: this.expiryTime,
         };
@@ -1581,6 +1606,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
             json.password,
             json.flow,
             json.email,
+            json.limitIp,
             json.totalGB,
             json.expiryTime,
 

+ 21 - 0
web/controller/inbound.go

@@ -31,6 +31,8 @@ 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)
 	g.POST("/resetClientTraffic/:email", a.resetClientTraffic)
 
 }
@@ -122,7 +124,26 @@ 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)
+}
 func (a *InboundController) resetClientTraffic(c *gin.Context) {
 	email := c.Param("email")
 

+ 1 - 1
web/html/login.html

@@ -39,7 +39,7 @@
         <a-layout-content>
             <a-row type="flex" justify="center">
                 <a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
-                    <h1>{{ i18n "pages.login.title" }}</h1>
+                    <h1>3x-ui {{ i18n "pages.login.title" }}</h1>
                 </a-col>
             </a-row>
             <a-row type="flex" justify="center">

+ 35 - 0
web/html/xui/form/protocol/trojan.html

@@ -21,6 +21,41 @@
                     </a-tooltip>
                 </span>
                 <a-input v-model.trim="trojan.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="trojan.limitIp" min="0" ></a-input>
+            </a-form-item>
+            <a-form-item v-if="trojan.email && trojan.limitIp > 0 && isEdit">
+                <span slot="label">
+                    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>
+                    <a-tooltip>
+                        <template slot="title">
+                            clear the log
+                        </template>
+                        <span style="color: #FF4D4F">
+                            <a-icon type="delete" @click="clearDBClientIps(trojan.email,$event)"></a-icon>
+                        </span>
+                    </a-tooltip>
+                </span>
+                <a-form layout="block">
+                    <a-textarea readonly @click="getDBClientIps(trojan.email,$event)" placeholder="Click To Get IPs"  :auto-size="{ minRows: 0.5, maxRows: 10 }">
+                    </a-textarea>
+                </a-form>
             </a-form-item>
         </a-form>
         <a-form-item label="Password">

+ 36 - 0
web/html/xui/form/protocol/vless.html

@@ -22,6 +22,42 @@
                     </a-tooltip>
                 </span>
                 <a-input v-model.trim="vless.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="vless.limitIp" min="0" ></a-input>
+            </a-form-item>
+            <a-form-item v-if="vless.email && vless.limitIp > 0 && isEdit">
+                <span slot="label">
+                    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>
+                    <a-tooltip>
+                        <template slot="title">
+                            clear the log
+                        </template>
+                        <span style="color: #FF4D4F">
+                            <a-icon type="delete" @click="clearDBClientIps(vless.email,$event)"></a-icon>
+                        </span>
+                    </a-tooltip>
+                </span>
+                <a-form layout="block">
+
+                    <a-textarea readonly @click="getDBClientIps(vless.email,$event)" placeholder="Click To Get IPs"  :auto-size="{ minRows: 0.5, maxRows: 10 }">
+                    </a-textarea>
+                </a-form>
             </a-form-item>
         </a-form>
         <a-form-item label="ID">

+ 33 - 0
web/html/xui/form/protocol/vmess.html

@@ -21,6 +21,39 @@
                     </a-tooltip>
                 </span>
                 <a-input v-model.trim="vmess.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="vmess.limitIp" min="0" ></a-input>
+            </a-form-item>
+            <a-form-item v-if="vmess.email && vmess.limitIp > 0 && isEdit">
+                <span slot="label">
+                    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>
+                    <a-tooltip>
+                        <template slot="title">
+                            clear the log
+                        </template>
+                        <span style="color: #FF4D4F">
+                            <a-icon type="delete" @click="clearDBClientIps(vmess.email,$event)"></a-icon>
+                        </span>
+                    </a-tooltip>
+                </span>
+                <a-textarea readonly @click="getDBClientIps(vmess.email,$event)" placeholder="Click To Get IPs"  :auto-size="{ minRows: 0.5, maxRows: 10 }">
+                </a-textarea>
             </a-form-item>
         </a-form>
         <a-form-item label="ID">

+ 24 - 0
web/html/xui/inbound_modal.html

@@ -88,6 +88,30 @@
             removeClient(index, clients) {
                 clients.splice(index, 1);
             },
+            async getDBClientIps(email,event) {
+
+                const msg = await HttpUtil.post('/xui/inbound/clientIps/'+ email);
+                if (!msg.success) {
+                    return;
+                }
+                try {
+                    ips = JSON.parse(msg.obj)
+                    ips = ips.join(",")
+                    event.target.value = ips
+                } catch (error) {
+                    // text
+                    event.target.value = msg.obj
+
+                }
+
+            },
+            async clearDBClientIps(email,event) {
+                const msg = await HttpUtil.post('/xui/inbound/clearClientIps/'+ email);
+                if (!msg.success) {
+                    return;
+                }
+                event.target.value = ""
+            },
             async resetClientTraffic(client,event) {
                 const msg = await HttpUtil.post('/xui/inbound/resetClientTraffic/'+ client.email);
                 if (!msg.success) {

+ 13 - 0
web/html/xui/inbounds.html

@@ -45,6 +45,7 @@
                     <a-card hoverable>
                         <div slot="title">
                              <a-button type="primary" @click="openAddInbound">Add Inbound</a-button>
+							 <a-button type="primary" @click="exportAllLinks" class="copy-btn">Export Links</a-button>
                         </div>
 						<a-input v-model.lazy="searchKey" placeholder="{{ i18n "search" }}" autofocus style="max-width: 300px"></a-input>
                         <a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
@@ -371,6 +372,18 @@
                     },
                 });
             },
+			exportAllLinks() {
+                    let copyText = '';
+                    for (const dbInbound of this.dbInbounds) {
+                        copyText += dbInbound.genInboundLinks
+                    }
+                    const clipboard = new ClipboardJS('.copy-btn', {
+                        text: function () {
+                            return copyText;
+                        }
+                    });
+                    clipboard.on('success', () => { this.$message.success('Export Links succeed'); });
+                },
             delInbound(dbInbound) {
                 this.$confirm({
                     title: '{{ i18n "pages.inbounds.deleteInbound"}}',

+ 351 - 0
web/job/check_clinet_ip_job.go

@@ -0,0 +1,351 @@
+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)
+
+	if inbound.Settings == "" {
+		logger.Debug("wrong data ",inbound)
+		return nil
+	}
+
+	settings := map[string][]model.Client{}
+	json.Unmarshal([]byte(inbound.Settings), &settings)
+	clients := settings["clients"]
+
+	for _, client := range clients {
+		if client.Email == clientEmail {
+			
+			limitIp := client.LimitIP
+			
+			if(limitIp < len(ips) && limitIp != 0 && inbound.Enable) {
+				
+				disAllowedIps = append(disAllowedIps,ips[limitIp:]...)
+			}
+		}
+	}
+	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
+}

+ 22 - 0
web/service/inbound.go

@@ -369,7 +369,29 @@ func (s *InboundService) UpdateClientStat(inboundId int, inboundSettings string)
 func (s *InboundService) DelClientStat(tx *gorm.DB, email string) error {
 	return tx.Where("email = ?", email).Delete(xray.ClientTraffic{}).Error
 }
+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
+}
 func (s *InboundService) ResetClientTraffic(clientEmail string) error {
 	db := database.GetDB()
 

+ 2 - 2
web/service/server.go

@@ -172,7 +172,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
 }
 
 func (s *ServerService) GetXrayVersions() ([]string, error) {
-	url := "https://api.github.com/repos/XTLS/Xray-core/releases"
+	url := "https://api.github.com/repos/mhsanaei/Xray-core/releases"
 	resp, err := http.Get(url)
 	if err != nil {
 		return nil, err
@@ -215,7 +215,7 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
 	}
 
 	fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
-	url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName)
+	url := fmt.Sprintf("https://github.com/mhsanaei/Xray-core/releases/download/%s/%s", version, fileName)
 	resp, err := http.Get(url)
 	if err != nil {
 		return "", err

+ 1 - 1
web/translation/translate.fa_IR.toml

@@ -54,7 +54,7 @@
 "link" = "دیگر"
 
 [pages.login]
-"title" = "ورود به سیستم X-UI"
+"title" = "ورود به سیستم"
 "loginAgain" = "مدت زمان استفاده به اتمام رسیده ، لطفا دوباره وارد شوید"
 
 [pages.login.toasts]

+ 3 - 0
web/web.go

@@ -310,6 +310,9 @@ func (s *Server) startTask() {
 	// Check the inbound traffic every 30 seconds that the traffic exceeds and expires
 	s.cron.AddJob("@every 30s", job.NewCheckInboundJob())
 
+	// check client ips from log file every 10 sec
+	s.cron.AddJob("@every 10s", job.NewCheckClientIpJob())
+
 	// Make a traffic condition every day, 8:30
 	var entry cron.EntryID
 	isTgbotenabled, err := s.settingService.GetTgbotenabled()

+ 1 - 1
xray/process.go

@@ -162,7 +162,7 @@ func (p *process) Start() (err error) {
 		return common.NewErrorf("Failed to write configuration file: %v", err)
 	}
 
-	cmd := exec.Command(GetBinaryPath(), "-c", configPath)
+	cmd := exec.Command(GetBinaryPath(), "-c", configPath, "-restrictedIPsPath", "./bin/blockedIPs")
 	p.cmd = cmd
 
 	stdReader, err := cmd.StdoutPipe()