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

@@ -42,7 +42,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/hossinasaadi/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
@@ -84,7 +84,7 @@ jobs:
mv xui-release x-ui
mkdir bin
cd bin
wget https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-arm64-v8a.zip
wget https://github.com/hossinasaadi/Xray-core/releases/latest/download/Xray-linux-arm64-v8a.zip
unzip Xray-linux-arm64-v8a.zip
rm -f Xray-linux-arm64-v8a.zip geoip.dat geosite.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
@@ -126,7 +126,7 @@ jobs:
mv xui-release x-ui
mkdir bin
cd bin
wget https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-s390x.zip
wget https://github.com/hossinasaadi/Xray-core/releases/latest/download/Xray-linux-s390x.zip
unzip Xray-linux-s390x.zip
rm -f Xray-linux-s390x.zip geoip.dat geosite.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
@@ -144,4 +144,4 @@ jobs:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: x-ui-linux-s390x.tar.gz
asset_name: x-ui-linux-s390x.tar.gz
asset_content_type: application/gzip
asset_content_type: application/gzip

4
.gitignore vendored
View File

@@ -8,4 +8,6 @@ x-ui-*.tar.gz
/release.sh
.sync*
main
.cache
release/
access.log
.cache

141
README.md
View File

@@ -1,31 +1,45 @@
# x-ui
支持多协议多用户的 xray 面板
xray panel supporting multi-protocol multi-user
# 功能介绍
# Features
- 系统状态监控
- 支持多用户多协议,网页可视化操作
- 支持的协议:vmessvlesstrojanshadowsocksdokodemo-doorsockshttp
- 支持配置更多传输配置
- 流量统计,限制流量,限制到期时间
- 可自定义 xray 配置模板
- 支持 https 访问面板(自备域名 + ssl 证书)
- 支持一键SSL证书申请且自动续签
- 更多高级配置项,详见面板
- System Status Monitoring
- Support multi-user multi-protocol, web page visualization operation
- Supported protocols: vmess, vless, trojan, shadowsocks, dokodemo-door, socks, http
- Support for configuring more transport configurations
- Traffic statistics, limit traffic, limit expiration time
- Customizable xray configuration templates
- Support https access panel (self-provided domain name + ssl certificate)
- Support one-click SSL certificate application and automatic renewal
- For more advanced configuration items, please refer to the panel
# 安装&升级
# Enable IP Restrictions Per Inbound
1 - open panel settings and tab xray related settings put this to first of json :
```
{
"log": {
"loglevel": "warning",
"access": "./access.log"
},
```
bash <(curl -Ls https://raw.githubusercontent.com/vaxilu/x-ui/master/install.sh)
- change access log path as you want
2 - add **IP limit and Email** for inbound(vmess & vless)
# Install & Upgrade
```
bash <(curl -Ls https://raw.githubusercontent.com/hossinasaadi/x-ui/master/install.sh)
```
## 手动安装&升级
## Manual install & upgrade
1. 首先从 https://github.com/vaxilu/x-ui/releases 下载最新的压缩包,一般选择 `amd64`架构
2. 然后将这个压缩包上传到服务器的 `/root/`目录下,并使用 `root`用户登录服务器
1. First download the latest compressed package from https://github.com/hossinasaadi/x-ui/releases , generally choose Architecture `amd64`
2. Then upload the compressed package to the server's `/root/` directory and `root` rootlog in to the server with user
> 如果你的服务器 cpu 架构不是 `amd64`,自行将命令中的 `amd64`替换为其他架构
> If your server cpu architecture is not `amd64` replace another architecture
```
cd /root/
@@ -40,17 +54,17 @@ systemctl enable x-ui
systemctl restart x-ui
```
## 使用docker安装
## Install using docker
> docker 教程与 docker 镜像由[Chasing66](https://github.com/Chasing66)提供
> This docker tutorial and docker image are provided by [Chasing66](https://github.com/Chasing66)
1. 安装docker
1. install docker
```shell
curl -fsSL https://get.docker.com | sh
```
2. 安装x-ui
2. install x-ui
```shell
mkdir x-ui && cd x-ui
@@ -61,83 +75,82 @@ docker run -itd --network=host \
enwaiax/x-ui:latest
```
> Build 自己的镜像
> Build your own image
```shell
docker build -t x-ui .
```
## SSL证书申请
## SSL certificate application
> 此功能与教程由[FranzKafkaYu](https://github.com/FranzKafkaYu)提供
> This feature and tutorial are provided by [FranzKafkaYu](https://github.com/FranzKafkaYu)
脚本内置SSL证书申请功能使用该脚本申请证书需满足以下条件:
The script has a built-in SSL certificate application function. To use this script to apply for a certificate, the following conditions must be met:
- 知晓Cloudflare 注册邮箱
- 知晓Cloudflare Global API Key
- 域名已通过cloudflare进行解析到当前服务器
- Know the Cloudflare registered email
- Know the Cloudflare Global API Key
- The domain name has been resolved to the current server through cloudflare
获取Cloudflare Global API Key的方法:
How to get the Cloudflare Global API Key:
![](media/bda84fbc2ede834deaba1c173a932223.png)
![](media/d13ffd6a73f938d1037d0708e31433bf.png)
使用时只需输入 `域名`, `邮箱`, `API KEY`即可,示意图如下
When using, just enter `email`, `domain`, `API KEY` and the schematic diagram is as follows
![](media/2022-04-04_141259.png)
注意事项:
Precautions:
- 该脚本使用DNS API进行证书申请
- 默认使用Let'sEncrypt作为CA方
- 证书安装目录为/root/cert目录
- 本脚本申请证书均为泛域名证书
- The script uses DNS API for certificate request
- By default, Let'sEncrypt is used as the CA party
- The certificate installation directory is the /root/cert directory
- The certificates applied for by this script are all generic domain name certificates
## Tg机器人使用(开发中,暂不可使用)
## Tg robot use (under development, temporarily unavailable)
> 此功能与教程由[FranzKafkaYu](https://github.com/FranzKafkaYu)提供
> This feature and tutorial are provided by [FranzKafkaYu](https://github.com/FranzKafkaYu)
X-UI支持通过Tg机器人实现每日流量通知面板登录提醒等功能使用Tg机器人需要自行申请
具体申请教程可以参考[博客链接](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html)
使用说明:在面板后台设置机器人相关参数,具体包括
X-UI supports daily traffic notification, panel login reminder and other functions through the Tg robot. To use the Tg robot, you need to apply for the specific application tutorial. You can refer to the [blog](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html)
Set the robot-related parameters in the panel background, including:
- Tg机器人Token
- Tg机器人ChatId
- Tg机器人周期运行时间采用crontab语法
- Tg Robot Token
- Tg Robot ChatId
- Tg robot cycle runtime, in crontab syntax
参考语法:
- 30 * * * * * //每一分的第30s进行通知
- @hourly //每小时通知
- @daily //每天通知(凌晨零点整)
- @every 8h //每8小时通知
TG通知内容
- 节点流量使用
- 面板登录提醒
- 节点到期提醒
- 流量预警提醒
Reference syntax:
更多功能规划中...
## 建议系统
- 30 * * * * * //Notify at the 30s of each point
- @hourly // hourly notification
- @daily // Daily notification (00:00 in the morning)
- @every 8h // notify every 8 hours
- TG notification content:
- Node traffic usage
- Panel login reminder
- Node expiration reminder
- Traffic warning reminder
More features are planned...
## suggestion system
- CentOS 7+
- Ubuntu 16+
- Debian 8+
# 常见问题
# common problem
## v2-ui 迁移
## Migrating from v2-ui
首先在安装了 v2-ui 的服务器上安装最新版 x-ui然后使用以下命令进行迁移将迁移本机 v2-ui `所有 inbound 账号数据`至 x-ui`面板设置和用户名密码不会迁移`
First install the latest version of x-ui on the server where v2-ui is installed, and then use the following command to migrate, which will migrate the native v2-ui `All inbound account data` to x-ui`Panel settings and username passwords are not migrated`
> 迁移成功后请 `关闭 v2-ui`并且 `重启 x-ui`,否则 v2-ui 的 inbound 会与 x-ui 的 inbound 会产生 `端口冲突`
> Please `Close v2-ui` and `restart x-ui`, otherwise the inbound of v2-ui will cause a `port conflict with the inbound of x-ui`
```
x-ui v2-ui
```
## issue 关闭
各种小白问题看得血压很高
## Stargazers over time
[![Stargazers over time](https://starchart.cc/vaxilu/x-ui.svg)](https://starchart.cc/vaxilu/x-ui)
[![Stargazers over time](https://starchart.cc/hossinasaadi/x-ui.svg)](https://starchart.cc/hossinasaadi/x-ui)

View File

@@ -40,6 +40,9 @@ func initInbound() error {
func initSetting() error {
return db.AutoMigrate(&model.Setting{})
}
func initInboundClientIps() error {
return db.AutoMigrate(&model.InboundClientIps{})
}
func InitDB(dbPath string) error {
dir := path.Dir(dbPath)
@@ -76,6 +79,10 @@ func InitDB(dbPath string) error {
if err != nil {
return err
}
err = initInboundClientIps()
if err != nil {
return err
}
return nil
}

View File

@@ -42,6 +42,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

1
go.mod
View File

@@ -8,6 +8,7 @@ require (
github.com/Workiva/go-datastructures v1.0.53
github.com/gin-contrib/sessions v0.0.3
github.com/gin-gonic/gin v1.7.1
github.com/go-cmd/cmd v1.4.1 // indirect
github.com/go-ole/go-ole v1.2.5 // indirect
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/nicksnyder/go-i18n/v2 v2.1.2

3
go.sum
View File

@@ -57,6 +57,8 @@ github.com/gin-gonic/gin v1.7.1 h1:qC89GU3p8TvKWMAVhEpmpB2CIb1hnqt2UdKZaP93mS8=
github.com/gin-gonic/gin v1.7.1/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
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-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
@@ -72,6 +74,7 @@ github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7a
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
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/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=

View File

@@ -107,20 +107,20 @@ install_x-ui() {
cd /usr/local/
if [ $# == 0 ]; then
last_version=$(curl -Ls "https://api.github.com/repos/vaxilu/x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
last_version=$(curl -Ls "https://api.github.com/repos/hossinasaadi/x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$last_version" ]]; then
echo -e "${red}检测 x-ui 版本失败,可能是超出 Github API 限制,请稍后再试,或手动指定 x-ui 版本安装${plain}"
exit 1
fi
echo -e "检测到 x-ui 最新版本:${last_version},开始安装"
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz https://github.com/vaxilu/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz https://github.com/hossinasaadi/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz
if [[ $? -ne 0 ]]; then
echo -e "${red}下载 x-ui 失败,请确保你的服务器能够下载 Github 的文件${plain}"
exit 1
fi
else
last_version=$1
url="https://github.com/vaxilu/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz"
url="https://github.com/hossinasaadi/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz"
echo -e "开始安装 x-ui v$1"
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz ${url}
if [[ $? -ne 0 ]]; then
@@ -138,7 +138,7 @@ install_x-ui() {
cd x-ui
chmod +x x-ui bin/xray-linux-${arch}
cp -f x-ui.service /etc/systemd/system/
wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/vaxilu/x-ui/main/x-ui.sh
wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/hossinasaadi/x-ui/main/x-ui.sh
chmod +x /usr/local/x-ui/x-ui.sh
chmod +x /usr/bin/x-ui
config_after_install

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()

View File

@@ -4,6 +4,7 @@ After=network.target
Wants=network.target
[Service]
Environment="xray.vmess.aead.forced=false"
Type=simple
WorkingDirectory=/usr/local/x-ui/
ExecStart=/usr/local/x-ui/x-ui

View File

@@ -94,7 +94,7 @@ before_show_menu() {
}
install() {
bash <(curl -Ls https://raw.githubusercontent.com/vaxilu/x-ui/master/install.sh)
bash <(curl -Ls https://raw.githubusercontent.com/hossinasaadi/x-ui/main/install.sh)
if [[ $? == 0 ]]; then
if [[ $# == 0 ]]; then
start
@@ -113,7 +113,7 @@ update() {
fi
return 0
fi
bash <(curl -Ls https://raw.githubusercontent.com/vaxilu/x-ui/master/install.sh)
bash <(curl -Ls https://raw.githubusercontent.com/hossinasaadi/x-ui/main/install.sh)
if [[ $? == 0 ]]; then
LOGI "更新完成,已自动重启面板 "
exit 0
@@ -302,7 +302,7 @@ install_bbr() {
}
update_shell() {
wget -O /usr/bin/x-ui -N --no-check-certificate https://github.com/vaxilu/x-ui/raw/master/x-ui.sh
wget -O /usr/bin/x-ui -N --no-check-certificate https://github.com/hossinasaadi/x-ui/raw/main/x-ui.sh
if [[ $? != 0 ]]; then
echo ""
LOGE "下载脚本失败,请检查本机能否连接 Github"