8 Commits
0.0.1 ... 0.1.0

Author SHA1 Message Date
sprov
d67dff5a4c Update README.md 2021-06-15 11:11:32 +08:00
sprov
e91daabb18 0.1.0
- 改进 ui 界面
 - 修复流量超出后账号不自动失效问题
 - 修复vless生成的链接不正确问题
 - 修复网页端重启面板功能问题
2021-06-15 11:10:39 +08:00
sprov
5cc4cf02ee Update README.md 2021-06-12 11:49:05 +08:00
sprov
1444a00630 Update README.md 2021-06-12 11:44:10 +08:00
sprov
ae4ea3e091 Update README.md 2021-06-12 11:39:34 +08:00
sprov
214b217f12 0.0.2
- 增加设置总流量功能,流量超出后自动禁用
 - 优化部分 ui 细节
 - 修复监听 ip 不为空导致无法启动 xray 的问题
 - 修复二维码链接没有包含 address 的问题
2021-06-12 11:26:35 +08:00
sprov
f6eb413597 fix inbound listen 2021-06-06 23:52:03 +08:00
sprov
e5788c6d8e install.sh 2021-06-06 23:41:30 +08:00
23 changed files with 458 additions and 87 deletions

View File

@@ -1,2 +1,41 @@
# x-ui
a web panel based on xray-core
支持多协议多用户的 xray 面板
# 功能介绍
- 系统状态监控
- 支持多用户多协议,网页可视化操作
- 支持的协议vmess、vless、trojan、shadowsocks、dokodemo-door、socks、http
- 支持配置更多传输配置
- 账号流量统计
- 可自定义 xray 配置模板
- 支持 https 访问面板(自备域名 + ssl 证书)
- 更多高级配置项,详见面板
# 安装&升级
## 测试版
```
bash <(curl -Ls https://raw.githubusercontent.com/sprov065/x-ui/master/install.sh) 0.1.0
```
## 建议系统
- CentOS 7+
- Ubuntu 16+
- Debian 8+
# 常见问题
## 与 v2-ui 关系
x-ui 相当于 v2-ui 的加强版,未来会加入更多功能,待 x-ui 功能稳定后v2-ui 将不再提供更新
x-ui 可与 v2-ui 并存,数据不互通,不影响对方的运行
## 从 v2-ui 迁移
将提供便捷的一键迁移方式,正在开发中
# Telegram
群组https://t.me/sprov_blog
频道https://t.me/sprov_channel
## Stargazers over time
[![Stargazers over time](https://starchart.cc/sprov065/x-ui.svg)](https://starchart.cc/sprov065/x-ui)

0
a.s
View File

View File

@@ -1,10 +1,18 @@
package config
import (
_ "embed"
"fmt"
"os"
"strings"
)
//go:embed version
var version string
//go:embed name
var name string
type LogLevel string
const (
@@ -15,11 +23,11 @@ const (
)
func GetVersion() string {
return "0.0.1"
return strings.TrimSpace(version)
}
func GetName() string {
return "x-ui"
return strings.TrimSpace(name)
}
func GetLogLevel() LogLevel {

1
config/name Normal file
View File

@@ -0,0 +1 @@
x-ui

1
config/version Normal file
View File

@@ -0,0 +1 @@
0.1.0

View File

@@ -26,8 +26,9 @@ type User struct {
type Inbound struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
UserId int `json:"-"`
Up int64 `json:"up"`
Down int64 `json:"down"`
Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"`
Total int64 `json:"total" form:"total"`
Remark string `json:"remark" form:"remark"`
Enable bool `json:"enable" form:"enable"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
@@ -43,8 +44,12 @@ type Inbound struct {
}
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
listen := i.Listen
if listen != "" {
listen = fmt.Sprintf("\"%v\"", listen)
}
return &xray.InboundConfig{
Listen: json_util.RawMessage(fmt.Sprintf("\"%s\"", i.Listen)),
Listen: json_util.RawMessage(listen),
Port: i.Port,
Protocol: string(i.Protocol),
Settings: json_util.RawMessage(i.Settings),

View File

@@ -112,7 +112,7 @@ install_x-ui() {
tar zxvf x-ui-linux-${arch}.tar.gz
rm x-ui-linux-${arch}.tar.gz -f
cd x-ui
chmod +x x-ui bin/xray-x-ui-linux-${arch}
chmod +x x-ui bin/xray-linux-${arch}
cp -f x-ui.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable x-ui

12
main.go
View File

@@ -18,9 +18,6 @@ import (
"x-ui/web/service"
)
// this function call global.setWebServer
func setWebServer(server global.WebServer)
func runWebServer() {
log.Printf("%v %v", config.GetName(), config.GetVersion())
@@ -45,7 +42,7 @@ func runWebServer() {
var server *web.Server
server = web.NewServer()
setWebServer(server)
global.SetWebServer(server)
err = server.Start()
if err != nil {
log.Println(err)
@@ -58,9 +55,12 @@ func runWebServer() {
sig := <-sigCh
if sig == syscall.SIGHUP {
server.Stop()
err := server.Stop()
if err != nil {
logger.Warning("stop server err:", err)
}
server = web.NewServer()
setWebServer(server)
global.SetWebServer(server)
err = server.Start()
if err != nil {
log.Println(err)

View File

@@ -26,6 +26,7 @@ class DBInbound {
userId = 0;
up = 0;
down = 0;
total = 0;
remark = "";
enable = true;
expiryTime = 0;
@@ -45,6 +46,46 @@ class DBInbound {
ObjectUtil.cloneProps(this, data);
}
get totalGB() {
return toFixed(this.total / ONE_GB, 2);
}
set totalGB(gb) {
this.total = toFixed(gb * ONE_GB, 0);
}
get isVMess() {
return this.protocol === Protocols.VMESS;
}
get isVLess() {
return this.protocol === Protocols.VLESS;
}
get isTrojan() {
return this.protocol === Protocols.TROJAN;
}
get isSS() {
return this.protocol === Protocols.SHADOWSOCKS;
}
get isSocks() {
return this.protocol === Protocols.SOCKS;
}
get isHTTP() {
return this.protocol === Protocols.HTTP;
}
get address() {
let address = location.hostname;
if (!ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0") {
address = this.listen;
}
return address;
}
toInbound() {
let settings = {};
if (!ObjectUtil.isEmpty(this.settings)) {
@@ -84,9 +125,9 @@ class DBInbound {
}
}
genLink(address = "") {
genLink() {
const inbound = this.toInbound();
return inbound.genLink(address, this.remark);
return inbound.genLink(this.address, this.remark);
}
}

View File

@@ -643,6 +643,81 @@ class Inbound extends XrayCommonClass {
this.stream.network = network;
}
// VMess & VLess
get uuid() {
switch (this.protocol) {
case Protocols.VMESS:
return this.settings.vmesses[0].id;
case Protocols.VLESS:
return this.settings.vlesses[0].id;
default:
return "";
}
}
// VLess
get flow() {
switch (this.protocol) {
case Protocols.VLESS:
return this.settings.vlesses[0].flow;
default:
return "";
}
}
// VMess
get alterId() {
switch (this.protocol) {
case Protocols.VMESS:
return this.settings.vmesses[0].alterId;
default:
return "";
}
}
// Socks & HTTP
get username() {
switch (this.protocol) {
case Protocols.SOCKS:
case Protocols.HTTP:
return this.settings.accounts[0].user;
default:
return "";
}
}
// Trojan & Shadowsocks & Socks & HTTP
get password() {
switch (this.protocol) {
case Protocols.TROJAN:
return this.settings.clients[0].password;
case Protocols.SHADOWSOCKS:
return this.settings.password;
case Protocols.SOCKS:
case Protocols.HTTP:
return this.settings.accounts[0].pass;
default:
return "";
}
}
// Shadowsocks
get method() {
switch (this.protocol) {
case Protocols.SHADOWSOCKS:
return this.settings.method;
default:
return "";
}
}
get serverName() {
if (this.stream.isTls || this.stream.isXTls) {
return this.stream.tls.server;
}
return "";
}
canEnableTls() {
switch (this.protocol) {
case Protocols.VMESS:
@@ -785,7 +860,7 @@ class Inbound extends XrayCommonClass {
const type = this.stream.network;
const params = new Map();
params.set("type", this.stream.network);
if (this.isXTls) {
if (this.xtls) {
params.set("security", "xtls");
} else {
params.set("security", this.stream.security);
@@ -841,7 +916,7 @@ class Inbound extends XrayCommonClass {
}
}
if (this.isXTls) {
if (this.xtls) {
params.set("flow", this.settings.vlesses[0].flow);
}

View File

@@ -284,7 +284,7 @@ class ObjectUtil {
return false;
}
}
return true
return true;
}
}

View File

@@ -36,8 +36,7 @@ func (a *InboundController) startTask() {
webServer := global.GetWebServer()
c := webServer.GetCron()
c.AddFunc("@every 10s", func() {
if a.xrayService.IsNeedRestart() {
a.xrayService.SetIsNeedRestart(false)
if a.xrayService.IsNeedRestartAndSetFalse() {
err := a.xrayService.RestartXray()
if err != nil {
logger.Error("restart xray failed:", err)
@@ -70,7 +69,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
err = a.inboundService.AddInbound(inbound)
jsonMsg(c, "添加", err)
if err == nil {
a.xrayService.SetIsNeedRestart(true)
a.xrayService.SetToNeedRestart()
}
}
@@ -83,7 +82,7 @@ func (a *InboundController) delInbound(c *gin.Context) {
err = a.inboundService.DelInbound(id)
jsonMsg(c, "删除", err)
if err == nil {
a.xrayService.SetIsNeedRestart(true)
a.xrayService.SetToNeedRestart()
}
}
@@ -104,6 +103,6 @@ func (a *InboundController) updateInbound(c *gin.Context) {
err = a.inboundService.UpdateInbound(inbound)
jsonMsg(c, "修改", err)
if err == nil {
a.xrayService.SetIsNeedRestart(true)
a.xrayService.SetToNeedRestart()
}
}

View File

@@ -13,8 +13,7 @@ type WebServer interface {
GetCtx() context.Context
}
//go:linkname setWebServer main.setWebServer
func setWebServer(s WebServer) {
func SetWebServer(s WebServer) {
webServer = s
}

View File

@@ -0,0 +1,71 @@
{{define "inboundInfoStream"}}
<p>传输: <a-tag color="green">[[ inbound.network ]]</a-tag></p>
<!-- TODO -->
<template v-if="inbound.tls || inbound.xtls">
<p v-if="inbound.tls">tls: <a-tag color="green">开启</a-tag></p>
<p v-if="inbound.xtls">xtls: <a-tag color="green">开启</a-tag></p>
</template>
<template v-else>
<p>tls: <a-tag color="red">关闭</a-tag></p>
</template>
<p v-if="inbound.tls">
tls域名: <a-tag color="green">[[ inbound.serverName ? inbound.serverName : "无" ]]</a-tag>
</p>
<p v-if="inbound.xtls">
xtls域名: <a-tag color="green">[[ inbound.serverName ? inbound.serverName : "无" ]]</a-tag>
</p>
{{end}}
{{define "component/inboundInfoComponent"}}
<div>
<p>协议: <a-tag color="green">[[ dbInbound.protocol ]]</a-tag></p>
<p>地址: <a-tag color="blue">[[ dbInbound.address ]]</a-tag></p>
<p>端口: <a-tag color="green">[[ dbInbound.port ]]</a-tag></p>
<template v-if="dbInbound.isVMess">
<p>uuid: <a-tag color="green">[[ inbound.uuid ]]</a-tag></p>
<p>alterId: <a-tag color="green">[[ inbound.alterId ]]</a-tag></p>
</template>
<template v-if="dbInbound.isVLess">
<p>uuid: <a-tag color="green">[[ inbound.uuid ]]</a-tag></p>
<p v-if="inbound.isXTls">flow: <a-tag color="green">[[ inbound.flow ]]</a-tag></p>
</template>
<template v-if="dbInbound.isTrojan">
<p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
</template>
<template v-if="dbInbound.isSS">
<p>加密: <a-tag color="green">[[ inbound.method ]]</a-tag></p>
<p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
</template>
<template v-if="dbInbound.isSocks">
<p>用户名: <a-tag color="green">[[ inbound.username ]]</a-tag></p>
<p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
</template>
<template v-if="dbInbound.isHTTP">
<p>用户名: <a-tag color="green">[[ inbound.username ]]</a-tag></p>
<p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
</template>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
{{template "inboundInfoStream"}}
</template>
</div>
{{end}}
{{define "component/inboundInfo"}}
<script>
Vue.component('inbound-info', {
delimiters: ['[[', ']]'],
props: ["dbInbound", "inbound"],
template: `{{template "component/inboundInfoComponent"}}`,
});
</script>
{{end}}

View File

@@ -27,6 +27,18 @@
<a-form-item label="端口">
<a-input type="number" v-model.number="inbound.port"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
总流量(GB)
<a-tooltip>
<template slot="title">
0 表示不限制
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
</a-form-item>
</a-form>
<!-- vmess settings -->

View File

@@ -3,7 +3,7 @@
<a-form-item label="id">
<a-input v-model.trim="inbound.settings.vlesses[0].id"></a-input>
</a-form-item>
<a-form-item label="flow">
<a-form-item v-if="inbound.xtls" label="flow">
<a-select v-model="inbound.settings.vlesses[0].flow" style="width: 150px">
<a-select-option value=""></a-select-option>
<a-select-option v-for="key in VLESS_FLOW" :value="key">[[ key ]]</a-select-option>

View File

@@ -0,0 +1,42 @@
{{define "inboundInfoModal"}}
{{template "component/inboundInfo"}}
<a-modal id="inbound-info-modal" v-model="infoModal.visible" title="详细信息" @ok="infoModal.ok"
:closable="true" :mask-closable="true"
ok-text="复制链接" cancel-text='{{ i18n "close" }}'>
<inbound-info :db-inbound="dbInbound" :inbound="inbound"></inbound-info>
</a-modal>
<script>
const infoModal = {
visible: false,
inbound: new Inbound(),
dbInbound: new DBInbound(),
ok() {
},
show(dbInbound) {
this.inbound = dbInbound.toInbound();
this.dbInbound = new DBInbound(dbInbound);
this.visible = true;
},
close() {
infoModal.visible = false;
},
};
new Vue({
delimiters: ['[[', ']]'],
el: '#inbound-info-modal',
data: {
infoModal,
get dbInbound() {
return this.infoModal.dbInbound;
},
get inbound() {
return this.infoModal.inbound;
}
},
});
</script>
{{end}}

View File

@@ -27,15 +27,15 @@
<a-card hoverable style="margin-bottom: 20px;">
<a-row>
<a-col :xs="24" :sm="24" :lg="12">
upload / download
总上传 / 下载
<a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag>
</a-col>
<a-col :xs="24" :sm="24" :lg="12">
total traffic
总用量
<a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag>
</a-col>
<a-col :xs="24" :sm="24" :lg="12">
number of accounts
入站数量
<a-tag color="green">[[ dbInbounds.length ]]</a-tag>
</a-col>
</a-row>
@@ -59,12 +59,19 @@
<template slot="traffic" slot-scope="text, dbInbound">
<a-tag color="blue">[[ sizeFormat(dbInbound.up) ]]</a-tag>
<a-tag color="green">[[ sizeFormat(dbInbound.down) ]]</a-tag>
<a-tag v-if="dbInbound.total > 0" color="cyan">[[ sizeFormat(dbInbound.total) ]]</a-tag>
<a-tag v-else color="cyan">无限制</a-tag>
</template>
<template slot="settings" slot-scope="text, dbInbound">
<a-button type="link">查看</a-button>
<a-button type="link" @click="showInfo(dbInbound)">查看</a-button>
</template>
<template slot="streamSettings" slot-scope="text, dbInbound">
<a-button type="link">查看</a-button>
<template slot="stream" slot-scope="text, dbInbound, index">
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag color="green">[[ inbounds[index].stream.network ]]</a-tag>
<a-tag v-if="inbounds[index].stream.isTls" color="blue">tls</a-tag>
<a-tag v-if="inbounds[index].stream.isXTls" color="blue">xtls</a-tag>
</template>
<template v-else></template>
</template>
<template slot="enable" slot-scope="text, dbInbound">
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound)"></a-switch>
@@ -76,6 +83,7 @@
<template slot="action" slot-scope="text, dbInbound">
<a-button v-if="dbInbound.hasLink()" type="primary" icon="qrcode" @click="showQrcode(dbInbound)"></a-button>
<a-button type="primary" icon="edit" @click="openEditInbound(dbInbound)"></a-button>
<a-button icon="retweet" @click="resetTraffic(dbInbound)"></a-button>
<a-button type="danger" icon="delete" @click="delInbound(dbInbound)"></a-button>
</template>
</a-table>
@@ -94,37 +102,37 @@
dataIndex: "id",
width: 60,
}, {
title: "protocol",
title: "协议",
align: 'center',
width: 60,
scopedSlots: { customRender: 'protocol' },
}, {
title: "port",
title: "端口",
align: 'center',
dataIndex: "port",
width: 60,
}, {
title: "traffic",
title: "流量↑|↓",
align: 'center',
width: 80,
scopedSlots: { customRender: 'traffic' },
}, {
title: "详细信息",
align: 'center',
width: 60,
scopedSlots: { customRender: 'traffic' },
// }, {
// title: "settings",
// align: 'center',
// width: 60,
// scopedSlots: { customRender: 'settings' },
// }, {
// title: "streamSettings",
// align: 'center',
// width: 60,
// scopedSlots: { customRender: 'streamSettings' },
scopedSlots: { customRender: 'settings' },
}, {
title: "enable",
title: "传输配置",
align: 'center',
width: 60,
scopedSlots: { customRender: 'stream' },
}, {
title: "启用",
align: 'center',
width: 60,
scopedSlots: { customRender: 'enable' },
}, {
title: "action",
title: "操作",
align: 'center',
width: 60,
scopedSlots: { customRender: 'action' },
@@ -136,6 +144,7 @@
data: {
siderDrawer,
spinning: false,
inbounds: [],
dbInbounds: [],
searchKey: '',
},
@@ -153,9 +162,12 @@
this.setInbounds(msg.obj);
},
setInbounds(dbInbounds) {
this.inbounds.splice(0);
this.dbInbounds.splice(0);
for (const inbound of dbInbounds) {
this.dbInbounds.push(new DBInbound(inbound));
const dbInbound = new DBInbound(inbound);
this.inbounds.push(dbInbound.toInbound());
this.dbInbounds.push(dbInbound);
}
},
searchInbounds(key) {
@@ -172,8 +184,8 @@
},
openAddInbound() {
inModal.show({
title: 'add account',
okText: 'add',
title: '添加入站',
okText: '添加',
confirm: async (inbound, dbInbound) => {
inModal.loading();
await this.addInbound(inbound, dbInbound);
@@ -184,8 +196,8 @@
openEditInbound(dbInbound) {
const inbound = dbInbound.toInbound();
inModal.show({
title: 'update account',
okText: 'update',
title: '修改入站',
okText: '修改',
inbound: inbound,
dbInbound: dbInbound,
confirm: async (inbound, dbInbound) => {
@@ -197,6 +209,9 @@
},
async addInbound(inbound, dbInbound) {
const data = {
up: dbInbound.up,
down: dbInbound.down,
total: dbInbound.total,
remark: dbInbound.remark,
enable: dbInbound.enable,
@@ -211,6 +226,9 @@
},
async updateInbound(inbound, dbInbound) {
const data = {
up: dbInbound.up,
down: dbInbound.down,
total: dbInbound.total,
remark: dbInbound.remark,
enable: dbInbound.enable,
@@ -223,23 +241,36 @@
};
await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal);
},
resetTraffic(dbInbound) {
this.$confirm({
title: '重置流量',
content: '确定要重置流量吗?',
okText: '重置',
cancelText: '取消',
onOk: () => {
const inbound = dbInbound.toInbound();
dbInbound.up = 0;
dbInbound.down = 0;
this.updateInbound(inbound, dbInbound);
},
});
},
delInbound(dbInbound) {
this.$confirm({
title: 'delete account',
content: 'Cannot be restored after deletion, confirm deletion?',
okText: 'delete',
cancelText: 'cancel',
title: '删除入站',
content: '确定要删除入站吗?',
okText: '删除',
cancelText: '取消',
onOk: () => this.submit('/xui/inbound/del/' + dbInbound.id),
});
},
showQrcode(dbInbound) {
let address = location.hostname;
if (!ObjectUtil.isEmpty(dbInbound.listen) || dbInbound.listen !== "0.0.0.0") {
address = dbInbound.listen;
}
const link = dbInbound.genLink(address);
const link = dbInbound.genLink();
qrModal.show('二维码', link);
},
showInfo(dbInbound) {
infoModal.show(dbInbound);
},
switchEnable(dbInbound) {
this.submit(`/xui/inbound/update/${dbInbound.id}`, dbInbound);
},
@@ -278,5 +309,6 @@
{{template "promptModal"}}
{{template "qrcodeModal"}}
{{template "textModal"}}
{{template "inboundInfoModal"}}
</body>
</html>

View File

@@ -38,11 +38,11 @@
<a-tabs default-active-key="1">
<a-tab-pane key="1" tab="面板配置">
<a-list item-layout="horizontal" style="background: white">
<setting-list-item type="text" title="面板监听 IP" desc="默认留空监听所有 IP" v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="number" title="面板监听端口" v-model.number="allSetting.webPort"></setting-list-item>
<setting-list-item type="text" title="面板证书公钥文件路径" desc="填写一个 '/' 开头的绝对路径" v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title="面板证书密钥文件路径" desc="填写一个 '/' 开头的绝对路径" v-model="allSetting.webKeyFile"></setting-list-item>
<setting-list-item type="text" title="面板 url 根路径" desc="必须以 '/' 开头,以 '/' 结尾" v-model="allSetting.webBasePath"></setting-list-item>
<setting-list-item type="text" title="面板监听 IP" desc="默认留空监听所有 IP,重启面板生效" v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="number" title="面板监听端口" desc="重启面板生效" v-model.number="allSetting.webPort"></setting-list-item>
<setting-list-item type="text" title="面板证书公钥文件路径" desc="填写一个 '/' 开头的绝对路径,重启面板生效" v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title="面板证书密钥文件路径" desc="填写一个 '/' 开头的绝对路径,重启面板生效" v-model="allSetting.webKeyFile"></setting-list-item>
<setting-list-item type="text" title="面板 url 根路径" desc="必须以 '/' 开头,以 '/' 结尾,重启面板生效" v-model="allSetting.webBasePath"></setting-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="2" tab="用户设置">
@@ -68,12 +68,12 @@
</a-tab-pane>
<a-tab-pane key="3" tab="xray 相关设置">
<a-list item-layout="horizontal" style="background: white">
<setting-list-item type="textarea" title="xray 配置模版" desc="以该模版为基础生成最终的 xray 配置文件" v-model="allSetting.xrayTemplateConfig"></setting-list-item>
<setting-list-item type="textarea" title="xray 配置模版" desc="以该模版为基础生成最终的 xray 配置文件,重启面板生效" v-model="allSetting.xrayTemplateConfig"></setting-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="4" tab="其他设置">
<a-list item-layout="horizontal" style="background: white">
<setting-list-item type="text" title="时区" desc="定时任务按照该时区的时间运行" v-model="allSetting.timeLocation"></setting-list-item>
<setting-list-item type="text" title="时区" desc="定时任务按照该时区的时间运行,重启面板生效" v-model="allSetting.timeLocation"></setting-list-item>
</a-list>
</a-tab-pane>
</a-tabs>

View File

@@ -56,6 +56,9 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) error {
if err != nil {
return err
}
oldInbound.Up = inbound.Up
oldInbound.Down = inbound.Down
oldInbound.Total = inbound.Total
oldInbound.Remark = inbound.Remark
oldInbound.Enable = inbound.Enable
oldInbound.ExpiryTime = inbound.ExpiryTime
@@ -98,3 +101,13 @@ func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
}
return
}
func (s *InboundService) DisableInvalidInbounds() (int64, error) {
db := database.GetDB()
result := db.Model(model.Inbound{}).
Where("up + down >= total and total > 0 and enable = ?", true).
Update("enable", false)
err := result.Error
count := result.RowsAffected
return count, err
}

View File

@@ -5,18 +5,18 @@ import (
"errors"
"go.uber.org/atomic"
"sync"
"x-ui/logger"
"x-ui/xray"
)
var p *xray.Process
var lock sync.Mutex
var isNeedXrayRestart atomic.Bool
var result string
type XrayService struct {
inboundService InboundService
settingService SettingService
isNeedXrayRestart atomic.Bool
}
func (s *XrayService) IsXrayRunning() bool {
@@ -87,6 +87,7 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, error) {
func (s *XrayService) RestartXray() error {
lock.Lock()
defer lock.Unlock()
logger.Debug("restart xray")
xrayConfig, err := s.GetXrayConfig()
if err != nil {
@@ -108,16 +109,17 @@ func (s *XrayService) RestartXray() error {
func (s *XrayService) StopXray() error {
lock.Lock()
defer lock.Unlock()
logger.Debug("stop xray")
if s.IsXrayRunning() {
return p.Stop()
}
return errors.New("xray is not running")
}
func (s *XrayService) SetIsNeedRestart(needRestart bool) {
s.isNeedXrayRestart.Store(needRestart)
func (s *XrayService) SetToNeedRestart() {
isNeedXrayRestart.Store(true)
}
func (s *XrayService) IsNeedRestart() bool {
return s.isNeedXrayRestart.Load()
func (s *XrayService) IsNeedRestartAndSetFalse() bool {
return isNeedXrayRestart.CAS(true, false)
}

View File

@@ -21,7 +21,6 @@ import (
"time"
"x-ui/config"
"x-ui/logger"
"x-ui/util"
"x-ui/util/common"
"x-ui/web/controller"
"x-ui/web/service"
@@ -45,7 +44,8 @@ func (f *wrapAssetsFS) Open(name string) (fs.File, error) {
}
type Server struct {
listener net.Listener
httpServer *http.Server
listener net.Listener
index *controller.IndexController
server *controller.ServerController
@@ -244,6 +244,7 @@ func (s *Server) startTask() {
logger.Warning("start xray failed:", err)
}
var checkTime = 0
// 每 30 秒检查一次 xray 是否在运行
s.cron.AddFunc("@every 30s", func() {
if s.xrayService.IsXrayRunning() {
checkTime = 0
@@ -253,11 +254,12 @@ func (s *Server) startTask() {
if checkTime < 2 {
return
}
s.xrayService.SetIsNeedRestart(true)
s.xrayService.SetToNeedRestart()
})
go func() {
time.Sleep(time.Second * 5)
// 与重启 xray 的时间错开
// 每 10 秒统计一次流量,首次启动延迟 5 秒,与重启 xray 的时间错开
s.cron.AddFunc("@every 10s", func() {
if !s.xrayService.IsXrayRunning() {
return
@@ -273,6 +275,17 @@ func (s *Server) startTask() {
}
})
}()
// 每 30 秒检查一次 inbound 流量超出情况
s.cron.AddFunc("@every 30s", func() {
count, err := s.inboundService.DisableInvalidInbounds()
if err != nil {
logger.Warning("disable invalid inbounds err:", err)
} else if count > 0 {
logger.Debugf("disabled %v inbounds", count)
s.xrayService.SetToNeedRestart()
}
})
}
func (s *Server) Start() (err error) {
@@ -337,24 +350,30 @@ func (s *Server) Start() (err error) {
s.startTask()
go engine.RunListener(listener)
s.httpServer = &http.Server{
Handler: engine,
}
go s.httpServer.Serve(listener)
return nil
}
func (s *Server) Stop() error {
if util.IsDone(s.ctx) {
// 防止 gc 后调用第二次 Stop
s.xrayService.StopXray()
}
s.cancel()
s.xrayService.StopXray()
if s.cron != nil {
s.cron.Stop()
}
if s.listener != nil {
return s.listener.Close()
var err1 error
var err2 error
if s.httpServer != nil {
err1 = s.httpServer.Shutdown(s.ctx)
}
return nil
if s.listener != nil {
err2 = s.listener.Close()
}
return common.Combine(err1, err2)
}
func (s *Server) GetCtx() context.Context {

12
x-ui.service Normal file
View File

@@ -0,0 +1,12 @@
[Unit]
Description=x-ui Service
After=network.target
Wants=network.target
[Service]
Type=simple
WorkingDirectory=/usr/local/x-ui/
ExecStart=/usr/local/x-ui/x-ui
[Install]
WantedBy=multi-user.target