Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/MHSanaei/3x-ui.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/release.yml3
-rw-r--r--database/db.go7
-rw-r--r--database/model/model.go6
-rw-r--r--go.mod1
-rw-r--r--go.sum3
-rw-r--r--web/assets/js/model/models.js12
-rw-r--r--web/assets/js/model/xray.js38
-rw-r--r--web/controller/inbound.go21
-rw-r--r--web/html/login.html2
-rw-r--r--web/html/xui/form/protocol/trojan.html35
-rw-r--r--web/html/xui/form/protocol/vless.html36
-rw-r--r--web/html/xui/form/protocol/vmess.html33
-rw-r--r--web/html/xui/inbound_modal.html24
-rw-r--r--web/html/xui/inbounds.html13
-rw-r--r--web/job/check_clinet_ip_job.go351
-rw-r--r--web/service/inbound.go22
-rw-r--r--web/service/server.go4
-rw-r--r--web/translation/translate.fa_IR.toml2
-rw-r--r--web/web.go3
-rw-r--r--xray/process.go2
20 files changed, 604 insertions, 14 deletions
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a054524f..fb74bfdc 100644
--- a/.github/workflows/release.yml
+++ b/.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
diff --git a/database/db.go b/database/db.go
index 15ff0e65..f7a590b2 100644
--- a/database/db.go
+++ b/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
diff --git a/database/model/model.go b/database/model/model.go
index 25e0cd41..f0036bdb 100644
--- a/database/model/model.go
+++ b/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"`
diff --git a/go.mod b/go.mod
index 5816e0e9..da538fed 100644
--- a/go.mod
+++ b/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
diff --git a/go.sum b/go.sum
index 4d703900..2479e119 100644
--- a/go.sum
+++ b/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=
diff --git a/web/assets/js/model/models.js b/web/assets/js/model/models.js
index 9fa5e7fc..80132a1e 100644
--- a/web/assets/js/model/models.js
+++ b/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 {
diff --git a/web/assets/js/model/xray.js b/web/assets/js/model/xray.js
index 23d73930..ea34e081 100644
--- a/web/assets/js/model/xray.js
+++ b/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,
diff --git a/web/controller/inbound.go b/web/controller/inbound.go
index 96c7ba18..8ec90ed4 100644
--- a/web/controller/inbound.go
+++ b/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")
diff --git a/web/html/login.html b/web/html/login.html
index 5138f15e..f2c2116c 100644
--- a/web/html/login.html
+++ b/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">
diff --git a/web/html/xui/form/protocol/trojan.html b/web/html/xui/form/protocol/trojan.html
index 6d43bc34..4bf57d7a 100644
--- a/web/html/xui/form/protocol/trojan.html
+++ b/web/html/xui/form/protocol/trojan.html
@@ -22,6 +22,41 @@
</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">
<a-input v-model.trim="trojan.password"></a-input>
diff --git a/web/html/xui/form/protocol/vless.html b/web/html/xui/form/protocol/vless.html
index fc7ffaa6..6d895f19 100644
--- a/web/html/xui/form/protocol/vless.html
+++ b/web/html/xui/form/protocol/vless.html
@@ -23,6 +23,42 @@
</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">
<a-input v-model.trim="vless.id"></a-input>
diff --git a/web/html/xui/form/protocol/vmess.html b/web/html/xui/form/protocol/vmess.html
index f7050a5e..bab0cb8b 100644
--- a/web/html/xui/form/protocol/vmess.html
+++ b/web/html/xui/form/protocol/vmess.html
@@ -22,6 +22,39 @@
</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">
<a-input v-model.trim="vmess.id"></a-input>
diff --git a/web/html/xui/inbound_modal.html b/web/html/xui/inbound_modal.html
index 80ea2286..54a64bf9 100644
--- a/web/html/xui/inbound_modal.html
+++ b/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) {
diff --git a/web/html/xui/inbounds.html b/web/html/xui/inbounds.html
index 14a61669..1f324846 100644
--- a/web/html/xui/inbounds.html
+++ b/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"}}',
diff --git a/web/job/check_clinet_ip_job.go b/web/job/check_clinet_ip_job.go
new file mode 100644
index 00000000..bf71116b
--- /dev/null
+++ b/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())
+
+ }
+ }