mirror of
https://github.com/alireza0/x-ui.git
synced 2026-03-14 05:23:09 +00:00
Merge branch 'main' into main
This commit is contained in:
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -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
4
.gitignore
vendored
@@ -8,4 +8,6 @@ x-ui-*.tar.gz
|
||||
/release.sh
|
||||
.sync*
|
||||
main
|
||||
.cache
|
||||
release/
|
||||
access.log
|
||||
.cache
|
||||
|
||||
141
README.md
141
README.md
@@ -1,31 +1,45 @@
|
||||
# x-ui
|
||||
|
||||
支持多协议多用户的 xray 面板
|
||||
xray panel supporting multi-protocol multi-user
|
||||
|
||||
# 功能介绍
|
||||
# Features
|
||||
|
||||
- 系统状态监控
|
||||
- 支持多用户多协议,网页可视化操作
|
||||
- 支持的协议:vmess、vless、trojan、shadowsocks、dokodemo-door、socks、http
|
||||
- 支持配置更多传输配置
|
||||
- 流量统计,限制流量,限制到期时间
|
||||
- 可自定义 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:
|
||||

|
||||

|
||||
|
||||
使用时只需输入 `域名`, `邮箱`, `API KEY`即可,示意图如下:
|
||||
When using, just enter `email`, `domain`, `API KEY` and the schematic diagram is as follows:
|
||||

|
||||
|
||||
注意事项:
|
||||
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
|
||||
|
||||
[](https://starchart.cc/vaxilu/x-ui)
|
||||
[](https://starchart.cc/hossinasaadi/x-ui)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
1
go.mod
@@ -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
3
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
350
web/job/check_clinet_ip_job.go
Normal file
350
web/job/check_clinet_ip_job.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
6
x-ui.sh
6
x-ui.sh
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user