31 Commits
0.1.0 ... 0.3.2

Author SHA1 Message Date
vaxilu
f335c8d39a Update version 2021-08-25 17:16:19 +08:00
vaxilu
36d9a43421 Update common_sider.html 2021-08-25 17:06:23 +08:00
vaxilu
5d118afb07 Update x-ui.sh 2021-08-25 16:59:11 +08:00
vaxilu
56c30d4d0c Merge pull request #74 from jukrb0x/patch-1
fix: workaround for installing bbr
2021-08-25 16:55:31 +08:00
sprov
4c4a70e742 fix vless link 2021-08-25 09:42:20 +08:00
vaxilu
581e3b835a Merge pull request #64 from GSWXXN/main
add fallbacks feature & flow control mode for trojan
2021-08-25 08:46:27 +08:00
Jabriel
ccfeb28492 fix: workaround for installing bbr
The former link for installing bbr is not found anymore.
2021-08-25 04:21:28 +08:00
GSWXXN
100bd29c92 add flow control mode for trojan
Signed-off-by: GSWXXN <819269088@qq.com>
2021-08-22 16:20:22 +08:00
GSWXXN
6ed818cd67 add fallbacks feature for trojan
Signed-off-by: GSWXXN <819269088@qq.com>
2021-08-21 01:06:01 +08:00
vaxilu
698bf1d390 Merge pull request #60 from tshipenchko/patch-1
Change sprov065 to vaxilu
2021-08-17 19:52:04 +08:00
Łukasz Tshipenchko
aeed8db419 Change sprov065 to vaxilu 2021-08-15 14:55:17 +06:00
vaxilu
10191887af Update README.md 2021-08-10 14:24:53 +08:00
sprov
b240b843c8 Update version
0.3.1
2021-07-26 22:02:35 +08:00
sprov
500b08b112 Update inbound.go
fix expiry time
2021-07-26 22:02:08 +08:00
sprov
64c5ecce99 Update install.sh 2021-07-26 14:15:34 +08:00
sprov
3e08b794be Update README.md 2021-07-26 14:07:25 +08:00
sprov
e473b7393e Update README.md 2021-07-26 14:07:03 +08:00
sprov
942ac7f562 Update README.md 2021-07-26 13:41:38 +08:00
sprov
292d5b89d4 0.3.0
- 增加到期时间限制
 - 新增配置面板 https 访问后,http 自动跳转 https(同端口)
 - 降低获取系统连接数的 cpu 使用率
 - 优化界面
 - VMess 协议 alterId 默认改为 0
 - 修复旧版本 iOS 系统白屏问题
 - 修复重启面板后 xray 没有启动的问题
2021-07-26 13:29:29 +08:00
sprov
f1057b1142 Update issue templates 2021-06-24 12:47:43 +08:00
sprov
b8da50dde7 Update issue templates 2021-06-24 12:47:16 +08:00
sprov
991e711ac1 Update issue templates 2021-06-24 12:46:00 +08:00
sprov
1468984c92 修复inbounds备注不显示 2021-06-17 12:00:08 +08:00
sprov
e8b0d2c839 Merge branch 'main' of https://github.com/sprov065/x-ui into main 2021-06-17 11:53:35 +08:00
sprov
00f9de6f1c Update x-ui.sh 2021-06-17 11:53:26 +08:00
sprov
51f80b445d Update README.md 2021-06-17 11:19:51 +08:00
sprov
4bf10cd8b7 Merge branch 'main' of https://github.com/sprov065/x-ui into main 2021-06-17 11:15:12 +08:00
sprov
a02f03f5d6 Update x-ui.sh 2021-06-17 11:15:00 +08:00
sprov
7a660506ea Update README.md 2021-06-17 11:14:04 +08:00
sprov
f0356ad786 Update README.md 2021-06-17 11:11:56 +08:00
sprov
89677c4fe1 0.2.0
- 优化 ui 界面
 - 优化网站加载速度
 - 新增从 v2-ui 迁移账号数据的功能
2021-06-17 11:05:43 +08:00
38 changed files with 1034 additions and 214 deletions

14
.github/ISSUE_TEMPLATE/------.md vendored Normal file
View File

@@ -0,0 +1,14 @@
---
name: 提问题点这里
about: issue 模板
title: ''
labels: ''
assignees: ''
---
任何由于自己的配置错误导致的情况请自行解决issues 只用于解决面板自身的 bug
如果你确定面板的功能实现有 bug请尽可能提供更多更精确的描述信息、复现方法与复现结果等等而不是草草一句话了事这对于问题的解决没有帮助
提问的艺术: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ dist/
x-ui-*.tar.gz
/x-ui
/release.sh
.sync*

View File

@@ -6,15 +6,33 @@
- 支持多用户多协议,网页可视化操作
- 支持的协议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
bash <(curl -Ls https://raw.githubusercontent.com/vaxilu/x-ui/master/install.sh)
```
## 手动安装&升级
1. 首先从 https://github.com/vaxilu/x-ui/releases 下载最新的压缩包,一般选择`amd64`架构
2. 然后将这个压缩包上传到服务器的`/root/`目录下,并使用`root`用户登录服务器
> 如果你的服务器 cpu 架构不是`amd64`,自行将命令中的`amd64`替换为其他架构
```
cd /root/
rm x-ui/ /usr/local/x-ui/ /usr/bin/x-ui -rf
tar zxvf x-ui-linux-amd64.tar.gz
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
cp x-ui/x-ui.sh /usr/bin/x-ui
cp -f x-ui/x-ui.service /etc/systemd/system/
mv x-ui/ /usr/local/
systemctl daemon-reload
systemctl enable x-ui
systemctl restart x-ui
```
## 建议系统
@@ -29,13 +47,12 @@ x-ui 相当于 v2-ui 的加强版,未来会加入更多功能,待 x-ui 功
x-ui 可与 v2-ui 并存,数据不互通,不影响对方的运行
## 从 v2-ui 迁移
将提供便捷的一键迁移方式,正在开发中
# Telegram
群组https://t.me/sprov_blog
频道https://t.me/sprov_channel
首先在安装了 v2-ui 的服务器上安装最新版 x-ui然后使用以下命令进行迁移将迁移本机 v2-ui 的`所有 inbound 账号数据`至 x-ui`面板设置和用户名密码不会迁移`
> 迁移成功后请`关闭 v2-ui` 并且`重启 x-ui`,否则 v2-ui 的 inbound 会与 x-ui 的 inbound 会产生`端口冲突`
```
x-ui v2-ui
```
## Stargazers over time
[![Stargazers over time](https://starchart.cc/sprov065/x-ui.svg)](https://starchart.cc/sprov065/x-ui)
[![Stargazers over time](https://starchart.cc/vaxilu/x-ui.svg)](https://starchart.cc/vaxilu/x-ui)

View File

@@ -1 +1 @@
0.1.0
0.3.2

View File

@@ -82,25 +82,22 @@ install_base() {
install_x-ui() {
systemctl stop x-ui
cd /usr/local/
if [[ -e /usr/local/x-ui/ ]]; then
rm /usr/local/x-ui/ -rf
fi
if [ $# == 0 ] ;then
last_version=$(curl -Ls "https://api.github.com/repos/sprov065/x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
last_version=$(curl -Ls "https://api.github.com/repos/vaxilu/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/sprov065/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/vaxilu/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/sprov065/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz"
url="https://github.com/vaxilu/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
@@ -109,11 +106,16 @@ install_x-ui() {
fi
fi
if [[ -e /usr/local/x-ui/ ]]; then
rm /usr/local/x-ui/ -rf
fi
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-linux-${arch}
chmod +x x-ui bin/xray-linux-${arch} x-ui.sh
cp -f x-ui.service /etc/systemd/system/
cp -f x-ui.sh /usr/bin/x-ui
systemctl daemon-reload
systemctl enable x-ui
systemctl start x-ui
@@ -125,8 +127,6 @@ install_x-ui() {
echo -e ""
echo -e "如果是更新面板,则按你之前的方式访问面板"
echo -e ""
curl -o /usr/bin/x-ui -Ls https://raw.githubusercontent.com/sprov065/x-ui/master/x-ui.sh
chmod +x /usr/bin/x-ui
echo -e "x-ui 管理脚本使用方法: "
echo -e "----------------------------------------------"
echo -e "x-ui - 显示管理菜单 (功能更多)"
@@ -137,6 +137,7 @@ install_x-ui() {
echo -e "x-ui enable - 设置 x-ui 开机自启"
echo -e "x-ui disable - 取消 x-ui 开机自启"
echo -e "x-ui log - 查看 x-ui 日志"
echo -e "x-ui v2-ui - 迁移本机器的 v2-ui 账号数据至 x-ui"
echo -e "x-ui update - 更新 x-ui 面板"
echo -e "x-ui install - 安装 x-ui 面板"
echo -e "x-ui uninstall - 卸载 x-ui 面板"

View File

@@ -7,16 +7,22 @@ import (
var logger *logging.Logger
func init() {
InitLogger(logging.INFO)
}
func InitLogger(level logging.Level) {
format := logging.MustStringFormatter(
`%{time:2006/01/02 15:04:05} %{level} - %{message}`,
)
logger = logging.MustGetLogger("x-ui")
newLogger := logging.MustGetLogger("x-ui")
backend := logging.NewLogBackend(os.Stderr, "", 0)
backendFormatter := logging.NewBackendFormatter(backend, format)
backendLeveled := logging.AddModuleLevel(backendFormatter)
backendLeveled.SetLevel(level, "")
logger.SetBackend(backendLeveled)
newLogger.SetBackend(backendLeveled)
logger = newLogger
}
func Debug(args ...interface{}) {

12
main.go
View File

@@ -50,11 +50,12 @@ func runWebServer() {
}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGHUP)
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL)
for {
sig := <-sigCh
if sig == syscall.SIGHUP {
switch sig {
case syscall.SIGHUP:
err := server.Stop()
if err != nil {
logger.Warning("stop server err:", err)
@@ -66,8 +67,9 @@ func runWebServer() {
log.Println(err)
return
}
} else {
continue
default:
server.Stop()
return
}
}
}
@@ -173,7 +175,7 @@ func main() {
}
err = v2ui.MigrateFromV2UI(dbPath)
if err != nil {
logger.Error("migrate from v2-ui failed:", err)
fmt.Println("migrate from v2-ui failed:", err)
}
case "setting":
err := settingCmd.Parse(os.Args[2:])

0
util/sys/a.s Normal file
View File

8
util/sys/psutil.go Normal file
View File

@@ -0,0 +1,8 @@
package sys
import (
_ "unsafe"
)
//go:linkname HostProc github.com/shirou/gopsutil/internal/common.HostProc
func HostProc(combineWith ...string) string

23
util/sys/sys_darwin.go Normal file
View File

@@ -0,0 +1,23 @@
// +build darwin
package sys
import (
"github.com/shirou/gopsutil/net"
)
func GetTCPCount() (int, error) {
stats, err := net.Connections("tcp")
if err != nil {
return 0, err
}
return len(stats), nil
}
func GetUDPCount() (int, error) {
stats, err := net.Connections("udp")
if err != nil {
return 0, err
}
return len(stats), nil
}

70
util/sys/sys_linux.go Normal file
View File

@@ -0,0 +1,70 @@
// +build linux
package sys
import (
"bytes"
"fmt"
"io"
"os"
)
func getLinesNum(filename string) (int, error) {
file, err := os.Open(filename)
if err != nil {
return 0, err
}
defer file.Close()
sum := 0
buf := make([]byte, 8192)
for {
n, err := file.Read(buf)
var buffPosition int
for {
i := bytes.IndexByte(buf[buffPosition:], '\n')
if i < 0 || n == buffPosition {
break
}
buffPosition += i + 1
sum++
}
if err == io.EOF {
return sum, nil
} else if err != nil {
return sum, err
}
}
}
func GetTCPCount() (int, error) {
root := HostProc()
tcp4, err := getLinesNum(fmt.Sprintf("%v/net/tcp", root))
if err != nil {
return tcp4, err
}
tcp6, err := getLinesNum(fmt.Sprintf("%v/net/tcp6", root))
if err != nil {
return tcp4 + tcp6, nil
}
return tcp4 + tcp6, nil
}
func GetUDPCount() (int, error) {
root := HostProc()
udp4, err := getLinesNum(fmt.Sprintf("%v/net/udp", root))
if err != nil {
return udp4, err
}
udp6, err := getLinesNum(fmt.Sprintf("%v/net/udp6", root))
if err != nil {
return udp4 + udp6, nil
}
return udp4 + udp6, nil
}

28
v2ui/db.go Normal file
View File

@@ -0,0 +1,28 @@
package v2ui
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var v2db *gorm.DB
func initDB(dbPath string) error {
c := &gorm.Config{
Logger: logger.Discard,
}
var err error
v2db, err = gorm.Open(sqlite.Open(dbPath), c)
if err != nil {
return err
}
return nil
}
func getV2Inbounds() ([]*V2Inbound, error) {
inbounds := make([]*V2Inbound, 0)
err := v2db.Model(V2Inbound{}).Find(&inbounds).Error
return inbounds, err
}

41
v2ui/models.go Normal file
View File

@@ -0,0 +1,41 @@
package v2ui
import "x-ui/database/model"
type V2Inbound struct {
Id int `gorm:"primaryKey;autoIncrement"`
Port int `gorm:"unique"`
Listen string
Protocol string
Settings string
StreamSettings string
Tag string `gorm:"unique"`
Sniffing string
Remark string
Up int64
Down int64
Enable bool
}
func (i *V2Inbound) TableName() string {
return "inbound"
}
func (i *V2Inbound) ToInbound(userId int) *model.Inbound {
return &model.Inbound{
UserId: userId,
Up: i.Up,
Down: i.Down,
Total: 0,
Remark: i.Remark,
Enable: i.Enable,
ExpiryTime: 0,
Listen: i.Listen,
Port: i.Port,
Protocol: model.Protocol(i.Protocol),
Settings: i.Settings,
StreamSettings: i.StreamSettings,
Tag: i.Tag,
Sniffing: i.Sniffing,
}
}

View File

@@ -1,7 +1,51 @@
package v2ui
import "errors"
import (
"fmt"
"x-ui/config"
"x-ui/database"
"x-ui/database/model"
"x-ui/util/common"
"x-ui/web/service"
)
func MigrateFromV2UI(dbPath string) error {
return errors.New("not support right now")
err := initDB(dbPath)
if err != nil {
return common.NewError("init v2-ui database failed:", err)
}
err = database.InitDB(config.GetDBPath())
if err != nil {
return common.NewError("init x-ui database failed:", err)
}
v2Inbounds, err := getV2Inbounds()
if err != nil {
return common.NewError("get v2-ui inbounds failed:", err)
}
if len(v2Inbounds) == 0 {
fmt.Println("migrate v2-ui inbounds success: 0")
return nil
}
userService := service.UserService{}
user, err := userService.GetFirstUser()
if err != nil {
return common.NewError("get x-ui user failed:", err)
}
inbounds := make([]*model.Inbound, 0)
for _, v2inbound := range v2Inbounds {
inbounds = append(inbounds, v2inbound.ToInbound(user.Id))
}
inboundService := service.InboundService{}
err = inboundService.AddInbounds(inbounds)
if err != nil {
return common.NewError("add x-ui inbounds failed:", err)
}
fmt.Println("migrate v2-ui inbounds success:", len(inbounds))
return nil
}

File diff suppressed because one or more lines are too long

View File

@@ -1,14 +1,18 @@
class User {
username = "";
password = "";
constructor() {
this.username = "";
this.password = "";
}
}
class Msg {
success = false;
msg = "";
obj = null;
constructor(success, msg, obj) {
this.success = false;
this.msg = "";
this.obj = null;
if (success != null) {
this.success = success;
}
@@ -22,24 +26,25 @@ class Msg {
}
class DBInbound {
id = 0;
userId = 0;
up = 0;
down = 0;
total = 0;
remark = "";
enable = true;
expiryTime = 0;
listen = "";
port = 0;
protocol = "";
settings = "";
streamSettings = "";
tag = "";
sniffing = "";
constructor(data) {
this.id = 0;
this.userId = 0;
this.up = 0;
this.down = 0;
this.total = 0;
this.remark = "";
this.enable = true;
this.expiryTime = 0;
this.listen = "";
this.port = 0;
this.protocol = "";
this.settings = "";
this.streamSettings = "";
this.tag = "";
this.sniffing = "";
if (data == null) {
return;
}
@@ -86,6 +91,25 @@ class DBInbound {
return address;
}
get _expiryTime() {
if (this.expiryTime === 0) {
return null;
}
return moment(this.expiryTime);
}
set _expiryTime(t) {
if (t == null) {
this.expiryTime = 0;
} else {
this.expiryTime = t.valueOf();
}
}
get isExpiry() {
return this.expiryTime < new Date().getTime();
}
toInbound() {
let settings = {};
if (!ObjectUtil.isEmpty(this.settings)) {
@@ -132,17 +156,18 @@ class DBInbound {
}
class AllSetting {
webListen = "";
webPort = 54321;
webCertFile = "";
webKeyFile = "";
webBasePath = "/";
xrayTemplateConfig = "";
timeLocation = "Asia/Shanghai";
constructor(data) {
this.webListen = "";
this.webPort = 54321;
this.webCertFile = "";
this.webKeyFile = "";
this.webBasePath = "/";
this.xrayTemplateConfig = "";
this.timeLocation = "Asia/Shanghai";
if (data == null) {
return
}

View File

@@ -40,7 +40,7 @@ const RULE_DOMAIN = {
SPEEDTEST: 'geosite:speedtest',
};
const VLESS_FLOW = {
const FLOW_CONTROL = {
ORIGIN: "xtls-rprx-origin",
DIRECT: "xtls-rprx-direct",
};
@@ -50,7 +50,7 @@ Object.freeze(VmessMethods);
Object.freeze(SSMethods);
Object.freeze(RULE_IP);
Object.freeze(RULE_DOMAIN);
Object.freeze(VLESS_FLOW);
Object.freeze(FLOW_CONTROL);
class XrayCommonClass {
@@ -168,6 +168,15 @@ TcpStreamSettings.TcpRequest = class extends XrayCommonClass {
this.headers.push({ name: name, value: value });
}
getHeader(name) {
for (const header of this.headers) {
if (header.name.toLowerCase() === name.toLowerCase()) {
return header.value;
}
}
return null;
}
removeHeader(index) {
this.headers.splice(index, 1);
}
@@ -294,6 +303,15 @@ class WsStreamSettings extends XrayCommonClass {
this.headers.push({ name: name, value: value });
}
getHeader(name) {
for (const header of this.headers) {
if (header.name.toLowerCase() === name.toLowerCase()) {
return header.value;
}
}
return null;
}
removeHeader(index) {
this.headers.splice(index, 1);
}
@@ -643,6 +661,30 @@ class Inbound extends XrayCommonClass {
this.stream.network = network;
}
get isTcp() {
return this.network === "tcp";
}
get isWs() {
return this.network === "ws";
}
get isKcp() {
return this.network === "kcp";
}
get isQuic() {
return this.network === "quic"
}
get isGrpc() {
return this.network === "grpc";
}
get isH2() {
return this.network === "http";
}
// VMess & VLess
get uuid() {
switch (this.protocol) {
@@ -655,11 +697,13 @@ class Inbound extends XrayCommonClass {
}
}
// VLess
// VLess & Trojan
get flow() {
switch (this.protocol) {
case Protocols.VLESS:
return this.settings.vlesses[0].flow;
case Protocols.TROJAN:
return this.settings.clients[0].flow;
default:
return "";
}
@@ -718,6 +762,52 @@ class Inbound extends XrayCommonClass {
return "";
}
get host() {
if (this.isTcp) {
return this.stream.tcp.request.getHeader("Host");
} else if (this.isWs) {
return this.stream.ws.getHeader("Host");
} else if (this.isH2) {
return this.stream.http.host[0];
}
return null;
}
get path() {
if (this.isTcp) {
return this.stream.tcp.request.path[0];
} else if (this.isWs) {
return this.stream.ws.path;
} else if (this.isH2) {
return this.stream.http.path[0];
}
return null;
}
get quicSecurity() {
return this.stream.quic.security;
}
get quicKey() {
return this.stream.quic.key;
}
get quicType() {
return this.stream.quic.type;
}
get kcpType() {
return this.stream.kcp.type;
}
get kcpSeed() {
return this.stream.kcp.seed;
}
get serviceName() {
return this.stream.grpc.serviceName;
}
canEnableTls() {
switch (this.protocol) {
case Protocols.VMESS:
@@ -920,18 +1010,6 @@ class Inbound extends XrayCommonClass {
params.set("flow", this.settings.vlesses[0].flow);
}
for (const [key, value] of params) {
switch (key) {
case "host":
case "path":
case "seed":
case "key":
case "alpn":
params.set(key, encodeURIComponent(value));
break;
}
}
const link = `vless://${uuid}@${address}:${port}`;
const url = new URL(link);
for (const [key, value] of params) {
@@ -1077,7 +1155,7 @@ Inbound.VmessSettings = class extends Inbound.Settings {
}
};
Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), alterId=64) {
constructor(id=RandomUtil.randomUUID(), alterId=0) {
super();
this.id = id;
this.alterId = alterId;
@@ -1129,7 +1207,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
};
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), flow=VLESS_FLOW.DIRECT) {
constructor(id=RandomUtil.randomUUID(), flow=FLOW_CONTROL.DIRECT) {
super();
this.id = id;
this.flow = flow;
@@ -1182,14 +1260,26 @@ Inbound.VLESSSettings.Fallback = class extends XrayCommonClass {
};
Inbound.TrojanSettings = class extends Inbound.Settings {
constructor(protocol, clients=[new Inbound.TrojanSettings.Client()]) {
constructor(protocol,
clients=[new Inbound.TrojanSettings.Client()],
fallbacks=[],) {
super(protocol);
this.clients = clients;
this.fallbacks = fallbacks;
}
addTrojanFallback() {
this.fallbacks.push(new Inbound.TrojanSettings.Fallback());
}
delTrojanFallback(index) {
this.fallbacks.splice(index, 1);
}
toJson() {
return {
clients: Inbound.TrojanSettings.toJsonArray(this.clients),
fallbacks: Inbound.TrojanSettings.toJsonArray(this.fallbacks),
};
}
@@ -1198,27 +1288,74 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
for (const c of json.clients) {
clients.push(Inbound.TrojanSettings.Client.fromJson(c));
}
return new Inbound.TrojanSettings(Protocols.TROJAN, clients);
return new Inbound.TrojanSettings(
Protocols.TROJAN,
clients,
Inbound.TrojanSettings.Fallback.fromJson(json.fallbacks),);
}
};
Inbound.TrojanSettings.Client = class extends XrayCommonClass {
constructor(password=RandomUtil.randomSeq(10)) {
constructor(password=RandomUtil.randomSeq(10), flow=FLOW_CONTROL.DIRECT) {
super();
this.password = password;
this.flow = flow;
}
toJson() {
return {
password: this.password,
flow: this.flow,
};
}
static fromJson(json={}) {
return new Inbound.TrojanSettings.Client(json.password);
return new Inbound.TrojanSettings.Client(
json.password,
json.flow,
);
}
};
Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
constructor(name="", alpn='', path='', dest='', xver=0) {
super();
this.name = name;
this.alpn = alpn;
this.path = path;
this.dest = dest;
this.xver = xver;
}
toJson() {
let xver = this.xver;
if (!Number.isInteger(xver)) {
xver = 0;
}
return {
name: this.name,
alpn: this.alpn,
path: this.path,
dest: this.dest,
xver: xver,
}
}
static fromJson(json=[]) {
const fallbacks = [];
for (let fallback of json) {
fallbacks.push(new Inbound.TrojanSettings.Fallback(
fallback.name,
fallback.alpn,
fallback.path,
fallback.dest,
fallback.xver,
))
}
return fallbacks;
}
};
Inbound.ShadowsocksSettings = class extends Inbound.Settings {
constructor(protocol,
method=SSMethods.AES_256_GCM,

View File

@@ -77,8 +77,7 @@ class PromiseUtil {
}
class RandomUtil {
static seq = [
const seq = [
'a', 'b', 'c', 'd', 'e', 'f', 'g',
'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't',
@@ -88,7 +87,9 @@ class RandomUtil {
'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z'
];
];
class RandomUtil {
static randomIntRange(min, max) {
return parseInt(Math.random() * (max - min) + min, 10);
@@ -101,7 +102,7 @@ class RandomUtil {
static randomSeq(count) {
let str = '';
for (let i = 0; i < count; ++i) {
str += this.seq[this.randomInt(62)];
str += seq[this.randomInt(62)];
}
return str;
}
@@ -109,7 +110,7 @@ class RandomUtil {
static randomLowerAndNum(count) {
let str = '';
for (let i = 0; i < count; ++i) {
str += this.seq[this.randomInt(36)];
str += seq[this.randomInt(36)];
}
return str;
}
@@ -121,7 +122,7 @@ class RandomUtil {
if (index <= 9) {
str += index;
} else {
str += this.seq[index - 10];
str += seq[index - 10];
}
}
return str;

View File

@@ -37,7 +37,7 @@ func (a *InboundController) startTask() {
c := webServer.GetCron()
c.AddFunc("@every 10s", func() {
if a.xrayService.IsNeedRestartAndSetFalse() {
err := a.xrayService.RestartXray()
err := a.xrayService.RestartXray(false)
if err != nil {
logger.Error("restart xray failed:", err)
}

View File

@@ -20,7 +20,7 @@
<a-icon type="link"></a-icon>
<span>其他</span>
</template>
<a-menu-item key="https://github.com/sprov065/x-ui/">
<a-menu-item key="https://github.com/vaxilu/x-ui/">
<a-icon type="github"></a-icon>
<span>Github</span>
</a-menu-item>

View File

@@ -1,7 +1,28 @@
{{define "inboundInfoStream"}}
<p>传输: <a-tag color="green">[[ inbound.network ]]</a-tag></p>
<!-- TODO -->
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
<p v-if="inbound.host">host: <a-tag color="green">[[ inbound.host ]]</a-tag></p>
<p v-else>host: <a-tag color="orange"></a-tag></p>
<p v-if="inbound.path">path: <a-tag color="green">[[ inbound.path ]]</a-tag></p>
<p v-else>path: <a-tag color="orange"></a-tag></p>
</template>
<template v-if="inbound.isQuic">
<p>quic 加密: <a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></p>
<p>quic 密码: <a-tag color="green">[[ inbound.quicKey ]]</a-tag></p>
<p>quic 伪装: <a-tag color="green">[[ inbound.quicType ]]</a-tag></p>
</template>
<template v-if="inbound.isKcp">
<p>kcp 加密: <a-tag color="green">[[ inbound.kcpType ]]</a-tag></p>
<p>kcp 密码: <a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></p>
</template>
<template v-if="inbound.isGrpc">
<p>grpc serviceName: <a-tag color="green">[[ inbound.serviceName ]]</a-tag></p>
</template>
<template v-if="inbound.tls || inbound.xtls">
<p v-if="inbound.tls">tls: <a-tag color="green">开启</a-tag></p>
@@ -11,10 +32,10 @@
<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>
tls域名: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : "无" ]]</a-tag>
</p>
<p v-if="inbound.xtls">
xtls域名: <a-tag color="green">[[ inbound.serverName ? inbound.serverName : "无" ]]</a-tag>
xtls域名: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : "无" ]]</a-tag>
</p>
{{end}}

View File

@@ -39,6 +39,19 @@
</span>
<a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item>
<span slot="label">
到期时间
<a-tooltip>
<template slot="title">
留空则永不到期
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>
</a-form>
<!-- vmess settings -->

View File

@@ -3,5 +3,47 @@
<a-form-item label="密码">
<a-input v-model.trim="inbound.settings.clients[0].password"></a-input>
</a-form-item>
<a-form-item v-if="inbound.xtls" label="flow">
<a-select v-model="inbound.settings.clients[0].flow" style="width: 150px">
<a-select-option value=""></a-select-option>
<a-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</a-form>
<a-form layout="inline">
<a-form-item label="fallbacks">
<a-row>
<a-button type="primary" size="small"
@click="inbound.settings.addTrojanFallback()">
+
</a-button>
</a-row>
</a-form-item>
</a-form>
<!-- trojan fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-divider>
fallback[[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-divider>
<a-form-item label="name">
<a-input v-model="fallback.name"></a-input>
</a-form-item>
<a-form-item label="alpn">
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>
<a-form-item label="path">
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label="dest">
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label="xver">
<a-input type="number" v-model.number="fallback.xver"></a-input>
</a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form>
{{end}}

View File

@@ -6,7 +6,7 @@
<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>
<a-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</a-form>

View File

@@ -2,7 +2,7 @@
{{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" }}'>
ok-text="复制链接" cancel-text='{{ i18n "close" }}' :ok-button-props="infoModal.okBtnPros">
<inbound-info :db-inbound="dbInbound" :inbound="inbound"></inbound-info>
</a-modal>
<script>
@@ -11,20 +11,39 @@
visible: false,
inbound: new Inbound(),
dbInbound: new DBInbound(),
ok() {
clipboard: null,
okBtnPros: {
attrs: {
id: "inbound-info-modal-ok-btn",
style: "",
},
},
show(dbInbound) {
this.inbound = dbInbound.toInbound();
this.dbInbound = new DBInbound(dbInbound);
this.visible = true;
if (dbInbound.hasLink()) {
this.okBtnPros.attrs.style = "";
} else {
this.okBtnPros.attrs.style = "display: none";
}
if (this.clipboard == null) {
infoModalApp.$nextTick(() => {
this.clipboard = new ClipboardJS(`#${this.okBtnPros.attrs.id}`, {
text: () => this.dbInbound.genLink(),
});
this.clipboard.on('success', () => app.$message.success('复制成功'));
});
}
},
close() {
infoModal.visible = false;
},
};
new Vue({
const infoModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#inbound-info-modal',
data: {

View File

@@ -46,21 +46,44 @@
<div slot="title">
<a-button type="primary" icon="plus" @click="openAddInbound"></a-button>
</div>
<a-input v-model="searchKey" placeholder="search" autofocus style="max-width: 300px"></a-input>
<!-- <a-input v-model="searchKey" placeholder="搜索" autofocus style="max-width: 300px"></a-input>-->
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
:data-source="dbInbounds"
:loading="spinning" :scroll="{ x: 1500 }"
:pagination="false"
style="margin-top: 20px"
@change="() => getDBInbounds()">
<template slot="action" slot-scope="text, dbInbound">
<a-dropdown :trigger="['click']">
<a @click="e => e.preventDefault()">操作</a>
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)">
<a-menu-item v-if="dbInbound.hasLink()" key="qrcode">
<a-icon type="qrcode"></a-icon>二维码
</a-menu-item>
<a-menu-item key="edit">
<a-icon type="edit"></a-icon>编辑
</a-menu-item>
<a-menu-item key="resetTraffic">
<a-icon type="retweet"></a-icon>重置流量
</a-menu-item>
<a-menu-item key="delete">
<span style="color: #FF4D4F">
<a-icon type="delete"></a-icon>删除
</span>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
<template slot="protocol" slot-scope="text, dbInbound">
<a-tag color="blue">[[ dbInbound.protocol ]]</a-tag>
</template>
<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>
<a-tag color="blue">[[ sizeFormat(dbInbound.up) ]] / [[ sizeFormat(dbInbound.down) ]]</a-tag>
<template v-if="dbInbound.total > 0">
<a-tag v-if="dbInbound.up + dbInbound.down < dbInbound.total" color="cyan">[[ sizeFormat(dbInbound.total) ]]</a-tag>
<a-tag v-else color="red">[[ sizeFormat(dbInbound.total) ]]</a-tag>
</template>
<a-tag v-else color="green">无限制</a-tag>
</template>
<template slot="settings" slot-scope="text, dbInbound">
<a-button type="link" @click="showInfo(dbInbound)">查看</a-button>
@@ -77,14 +100,15 @@
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound)"></a-switch>
</template>
<template slot="expiryTime" slot-scope="text, dbInbound">
<span v-if="dbInbound.expiryTime > 0" color="red">[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]</span>
<span v-else>无限期</span>
<template v-if="dbInbound.expiryTime > 0">
<a-tag v-if="dbInbound.isExpiry" color="red">
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
</a-tag>
<a-tag v-else color="blue">
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
</a-tag>
</template>
<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>
<a-tag v-else color="green">无限期</a-tag>
</template>
</a-table>
</a-card>
@@ -97,10 +121,25 @@
<script>
const columns = [{
title: "操作",
align: 'center',
width: 30,
scopedSlots: { customRender: 'action' },
}, {
title: "启用",
align: 'center',
width: 40,
scopedSlots: { customRender: 'enable' },
}, {
title: "id",
align: 'center',
dataIndex: "id",
width: 60,
width: 30,
}, {
title: "备注",
align: 'center',
width: 100,
dataIndex: "remark",
}, {
title: "协议",
align: 'center',
@@ -114,12 +153,12 @@
}, {
title: "流量↑|↓",
align: 'center',
width: 80,
width: 150,
scopedSlots: { customRender: 'traffic' },
}, {
title: "详细信息",
align: 'center',
width: 60,
width: 40,
scopedSlots: { customRender: 'settings' },
}, {
title: "传输配置",
@@ -127,15 +166,10 @@
width: 60,
scopedSlots: { customRender: 'stream' },
}, {
title: "启用",
title: "到期时间",
align: 'center',
width: 60,
scopedSlots: { customRender: 'enable' },
}, {
title: "操作",
align: 'center',
width: 60,
scopedSlots: { customRender: 'action' },
width: 80,
scopedSlots: { customRender: 'expiryTime' },
}];
const app = new Vue({
@@ -182,6 +216,22 @@
});
}
},
clickAction(action, dbInbound) {
switch (action.key) {
case "qrcode":
this.showQrcode(dbInbound);
break;
case "edit":
this.openEditInbound(dbInbound);
break;
case "resetTraffic":
this.resetTraffic(dbInbound);
break;
case "delete":
this.delInbound(dbInbound);
break;
}
},
openAddInbound() {
inModal.show({
title: '添加入站',
@@ -214,6 +264,7 @@
total: dbInbound.total,
remark: dbInbound.remark,
enable: dbInbound.enable,
expiryTime: dbInbound.expiryTime,
listen: inbound.listen,
port: inbound.port,
@@ -231,6 +282,7 @@
total: dbInbound.total,
remark: dbInbound.remark,
enable: dbInbound.enable,
expiryTime: dbInbound.expiryTime,
listen: inbound.listen,
port: inbound.port,

View File

@@ -188,8 +188,6 @@
Object.freeze(State);
class CurTotal {
current = 0
total = 0
constructor(current, total) {
this.current = current;
@@ -216,19 +214,19 @@
}
class Status {
cpu = new CurTotal(0, 0);
disk = new CurTotal(0, 0);
loads = [0, 0, 0];
mem = new CurTotal(0, 0);
netIO = {up: 0, down: 0};
netTraffic = {sent: 0, recv: 0};
swap = new CurTotal(0, 0);
tcpCount = 0;
udpCount = 0;
uptime = 0;
xray = {state: State.Stop, errorMsg: "", version: "", color: ""};
constructor(data) {
this.cpu = new CurTotal(0, 0);
this.disk = new CurTotal(0, 0);
this.loads = [0, 0, 0];
this.mem = new CurTotal(0, 0);
this.netIO = {up: 0, down: 0};
this.netTraffic = {sent: 0, recv: 0};
this.swap = new CurTotal(0, 0);
this.tcpCount = 0;
this.udpCount = 0;
this.uptime = 0;
this.xray = {state: State.Stop, errorMsg: "", version: "", color: ""};
if (data == null) {
return;
}

View File

@@ -0,0 +1,25 @@
package job
import (
"x-ui/logger"
"x-ui/web/service"
)
type CheckInboundJob struct {
xrayService service.XrayService
inboundService service.InboundService
}
func NewCheckInboundJob() *CheckInboundJob {
return new(CheckInboundJob)
}
func (j *CheckInboundJob) Run() {
count, err := j.inboundService.DisableInvalidInbounds()
if err != nil {
logger.Warning("disable invalid inbounds err:", err)
} else if count > 0 {
logger.Debugf("disabled %v inbounds", count)
j.xrayService.SetToNeedRestart()
}
}

View File

@@ -0,0 +1,25 @@
package job
import "x-ui/web/service"
type CheckXrayRunningJob struct {
xrayService service.XrayService
checkTime int
}
func NewCheckXrayRunningJob() *CheckXrayRunningJob {
return new(CheckXrayRunningJob)
}
func (j *CheckXrayRunningJob) Run() {
if j.xrayService.IsXrayRunning() {
j.checkTime = 0
return
}
j.checkTime++
if j.checkTime < 2 {
return
}
j.xrayService.SetToNeedRestart()
}

View File

@@ -0,0 +1,30 @@
package job
import (
"x-ui/logger"
"x-ui/web/service"
)
type XrayTrafficJob struct {
xrayService service.XrayService
inboundService service.InboundService
}
func NewXrayTrafficJob() *XrayTrafficJob {
return new(XrayTrafficJob)
}
func (j *XrayTrafficJob) Run() {
if !j.xrayService.IsXrayRunning() {
return
}
traffics, err := j.xrayService.GetXrayTraffic()
if err != nil {
logger.Warning("get xray traffic failed:", err)
return
}
err = j.inboundService.AddTraffic(traffics)
if err != nil {
logger.Warning("add traffic failed:", err)
}
}

View File

@@ -0,0 +1,21 @@
package network
import "net"
type AutoHttpsListener struct {
net.Listener
}
func NewAutoHttpsListener(listener net.Listener) net.Listener {
return &AutoHttpsListener{
Listener: listener,
}
}
func (l *AutoHttpsListener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
return NewAutoHttpsConn(conn), nil
}

View File

@@ -0,0 +1,67 @@
package network
import (
"bufio"
"bytes"
"fmt"
"net"
"net/http"
"sync"
)
type AutoHttpsConn struct {
net.Conn
firstBuf []byte
bufStart int
readRequestOnce sync.Once
}
func NewAutoHttpsConn(conn net.Conn) net.Conn {
return &AutoHttpsConn{
Conn: conn,
}
}
func (c *AutoHttpsConn) readRequest() bool {
c.firstBuf = make([]byte, 2048)
n, err := c.Conn.Read(c.firstBuf)
c.firstBuf = c.firstBuf[:n]
if err != nil {
return false
}
reader := bytes.NewReader(c.firstBuf)
bufReader := bufio.NewReader(reader)
request, err := http.ReadRequest(bufReader)
if err != nil {
return false
}
resp := http.Response{
Header: http.Header{},
}
resp.StatusCode = http.StatusTemporaryRedirect
location := fmt.Sprintf("https://%v%v", request.Host, request.RequestURI)
resp.Header.Set("Location", location)
resp.Write(c.Conn)
c.Close()
c.firstBuf = nil
return true
}
func (c *AutoHttpsConn) Read(buf []byte) (int, error) {
c.readRequestOnce.Do(func() {
c.readRequest()
})
if c.firstBuf != nil {
n := copy(buf, c.firstBuf[c.bufStart:])
c.bufStart += n
if c.bufStart >= len(c.firstBuf) {
c.firstBuf = nil
}
return n, nil
}
return c.Conn.Read(buf)
}

View File

@@ -3,8 +3,10 @@ package service
import (
"fmt"
"gorm.io/gorm"
"time"
"x-ui/database"
"x-ui/database/model"
"x-ui/util/common"
"x-ui/xray"
)
@@ -31,11 +33,64 @@ func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
return inbounds, nil
}
func (s *InboundService) checkPortExist(port int, ignoreId int) (bool, error) {
db := database.GetDB()
db = db.Model(model.Inbound{}).Where("port = ?", port)
if ignoreId > 0 {
db = db.Where("id != ?", ignoreId)
}
var count int64
err := db.Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (s *InboundService) AddInbound(inbound *model.Inbound) error {
exist, err := s.checkPortExist(inbound.Port, 0)
if err != nil {
return err
}
if exist {
return common.NewError("端口已存在:", inbound.Port)
}
db := database.GetDB()
return db.Save(inbound).Error
}
func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
for _, inbound := range inbounds {
exist, err := s.checkPortExist(inbound.Port, 0)
if err != nil {
return err
}
if exist {
return common.NewError("端口已存在:", inbound.Port)
}
}
db := database.GetDB()
tx := db.Begin()
var err error
defer func() {
if err == nil {
tx.Commit()
} else {
tx.Rollback()
}
}()
for _, inbound := range inbounds {
err = tx.Save(inbound).Error
if err != nil {
return err
}
}
return nil
}
func (s *InboundService) DelInbound(id int) error {
db := database.GetDB()
return db.Delete(model.Inbound{}, id).Error
@@ -52,6 +107,14 @@ func (s *InboundService) GetInbound(id int) (*model.Inbound, error) {
}
func (s *InboundService) UpdateInbound(inbound *model.Inbound) error {
exist, err := s.checkPortExist(inbound.Port, inbound.Id)
if err != nil {
return err
}
if exist {
return common.NewError("端口已存在:", inbound.Port)
}
oldInbound, err := s.GetInbound(inbound.Id)
if err != nil {
return err
@@ -104,8 +167,9 @@ func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
func (s *InboundService) DisableInvalidInbounds() (int64, error) {
db := database.GetDB()
now := time.Now().Unix() * 1000
result := db.Model(model.Inbound{}).
Where("up + down >= total and total > 0 and enable = ?", true).
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
Update("enable", false)
err := result.Error
count := result.RowsAffected

View File

@@ -18,6 +18,7 @@ import (
"runtime"
"time"
"x-ui/logger"
"x-ui/util/sys"
"x-ui/xray"
)
@@ -142,18 +143,14 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
logger.Warning("can not find io counters")
}
tcpConnStats, err := net.Connections("tcp")
status.TcpCount, err = sys.GetTCPCount()
if err != nil {
logger.Warning("get connections failed:", err)
} else {
status.TcpCount = len(tcpConnStats)
logger.Warning("get tcp connections failed:", err)
}
udpConnStats, err := net.Connections("udp")
status.UdpCount, err = sys.GetUDPCount()
if err != nil {
logger.Warning("get connections failed:", err)
} else {
status.UdpCount = len(udpConnStats)
logger.Warning("get udp connections failed:", err)
}
if s.xrayService.IsXrayRunning() {
@@ -265,7 +262,7 @@ func (s *ServerService) UpdateXray(version string) error {
s.xrayService.StopXray()
defer func() {
err := s.xrayService.RestartXray()
err := s.xrayService.RestartXray(true)
if err != nil {
logger.Error("start xray failed:", err)
}

View File

@@ -11,6 +11,19 @@ import (
type UserService struct {
}
func (s *UserService) GetFirstUser() (*model.User, error) {
db := database.GetDB()
user := &model.User{}
err := db.Model(model.User{}).
First(user).
Error
if err != nil {
return nil, err
}
return user, nil
}
func (s *UserService) CheckUser(username string, password string) *model.User {
db := database.GetDB()

View File

@@ -84,18 +84,19 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, error) {
return p.GetTraffic(true)
}
func (s *XrayService) RestartXray() error {
func (s *XrayService) RestartXray(isForce bool) error {
lock.Lock()
defer lock.Unlock()
logger.Debug("restart xray")
logger.Debug("restart xray, force:", isForce)
xrayConfig, err := s.GetXrayConfig()
if err != nil {
return err
}
if p != nil {
if p.GetConfig().Equals(xrayConfig) {
if p != nil && p.IsRunning() {
if !isForce && p.GetConfig().Equals(xrayConfig) {
logger.Debug("not need to restart xray")
return nil
}
p.Stop()

View File

@@ -18,11 +18,14 @@ import (
"net/http"
"os"
"strconv"
"strings"
"time"
"x-ui/config"
"x-ui/logger"
"x-ui/util/common"
"x-ui/web/controller"
"x-ui/web/job"
"x-ui/web/network"
"x-ui/web/service"
)
@@ -35,12 +38,42 @@ var htmlFS embed.FS
//go:embed translation/*
var i18nFS embed.FS
var startTime = time.Now()
type wrapAssetsFS struct {
embed.FS
}
func (f *wrapAssetsFS) Open(name string) (fs.File, error) {
return f.FS.Open("assets/" + name)
file, err := f.FS.Open("assets/" + name)
if err != nil {
return nil, err
}
return &wrapAssetsFile{
File: file,
}, nil
}
type wrapAssetsFile struct {
fs.File
}
func (f *wrapAssetsFile) Stat() (fs.FileInfo, error) {
info, err := f.File.Stat()
if err != nil {
return nil, err
}
return &wrapAssetsFileInfo{
FileInfo: info,
}, nil
}
type wrapAssetsFileInfo struct {
fs.FileInfo
}
func (f *wrapAssetsFileInfo) ModTime() time.Time {
return startTime
}
type Server struct {
@@ -131,12 +164,19 @@ func (s *Server) initRouter() (*gin.Engine, error) {
if err != nil {
return nil, err
}
assetsBasePath := basePath + "assets/"
store := cookie.NewStore(secret)
engine.Use(sessions.Sessions("session", store))
engine.Use(func(c *gin.Context) {
c.Set("base_path", basePath)
})
engine.Use(func(c *gin.Context) {
uri := c.Request.RequestURI
if strings.HasPrefix(uri, assetsBasePath) {
c.Header("Cache-Control", "max-age=31536000")
}
})
err = s.initI18n(engine)
if err != nil {
return nil, err
@@ -239,53 +279,21 @@ func (s *Server) initI18n(engine *gin.Engine) error {
}
func (s *Server) startTask() {
err := s.xrayService.RestartXray()
err := s.xrayService.RestartXray(true)
if err != nil {
logger.Warning("start xray failed:", err)
}
var checkTime = 0
// 每 30 秒检查一次 xray 是否在运行
s.cron.AddFunc("@every 30s", func() {
if s.xrayService.IsXrayRunning() {
checkTime = 0
return
}
checkTime++
if checkTime < 2 {
return
}
s.xrayService.SetToNeedRestart()
})
s.cron.AddJob("@every 30s", job.NewCheckXrayRunningJob())
go func() {
time.Sleep(time.Second * 5)
// 每 10 秒统计一次流量,首次启动延迟 5 秒,与重启 xray 的时间错开
s.cron.AddFunc("@every 10s", func() {
if !s.xrayService.IsXrayRunning() {
return
}
traffics, err := s.xrayService.GetXrayTraffic()
if err != nil {
logger.Warning("get xray traffic failed:", err)
return
}
err = s.inboundService.AddTraffic(traffics)
if err != nil {
logger.Warning("add traffic failed:", err)
}
})
s.cron.AddJob("@every 10s", job.NewXrayTrafficJob())
}()
// 每 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()
}
})
// 每 30 秒检查一次 inbound 流量超出和到期的情况
s.cron.AddJob("@every 30s", job.NewCheckInboundJob())
}
func (s *Server) Start() (err error) {
@@ -324,22 +332,21 @@ func (s *Server) Start() (err error) {
return err
}
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
var listener net.Listener
if certFile != "" || keyFile != "" {
var cert tls.Certificate
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
listener, err := net.Listen("tcp", listenAddr)
if err != nil {
return err
}
if certFile != "" || keyFile != "" {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
listener.Close()
return err
}
c := &tls.Config{
Certificates: []tls.Certificate{cert},
}
listener, err = tls.Listen("tcp", listenAddr, c)
} else {
listener, err = net.Listen("tcp", listenAddr)
}
if err != nil {
return err
listener = network.NewAutoHttpsListener(listener)
listener = tls.NewListener(listener, c)
}
if certFile != "" || keyFile != "" {
logger.Info("web server run https on", listener.Addr())
@@ -354,7 +361,9 @@ func (s *Server) Start() (err error) {
Handler: engine,
}
go s.httpServer.Serve(listener)
go func() {
s.httpServer.Serve(listener)
}()
return nil
}

22
x-ui.sh
View File

@@ -82,7 +82,7 @@ before_show_menu() {
}
install() {
bash <(curl -Ls https://blog.sprov.xyz/x-ui.sh)
bash <(curl -Ls https://raw.githubusercontent.com/vaxilu/x-ui/master/install.sh)
if [[ $? == 0 ]]; then
if [[ $# == 0 ]]; then
start
@@ -101,7 +101,7 @@ update() {
fi
return 0
fi
bash <(curl -Ls https://blog.sprov.xyz/x-ui.sh)
bash <(curl -Ls https://raw.githubusercontent.com/vaxilu/x-ui/master/install.sh)
if [[ $? == 0 ]]; then
echo -e "${green}更新完成,已自动重启面板${plain}"
exit 0
@@ -127,9 +127,6 @@ uninstall() {
echo ""
echo -e "卸载成功,如果你想删除此脚本,则退出脚本后运行 ${green}rm /usr/bin/x-ui -f${plain} 进行删除"
echo ""
echo -e "Telegram 群组: ${green}https://t.me/sprov_blog${plain}"
echo -e "Github issues: ${green}https://github.com/sprov065/x-ui/issues${plain}"
echo -e "博客: ${green}https://blog.sprov.xyz/x-ui${plain}"
if [[ $# == 0 ]]; then
before_show_menu
@@ -270,14 +267,21 @@ show_log() {
fi
}
migrate_v2_ui() {
/usr/local/x-ui/x-ui v2-ui
before_show_menu
}
install_bbr() {
bash <(curl -L -s https://raw.githubusercontent.com/sprov065/blog/master/bbr.sh)
# temporary workaround for installing bbr
bash <(curl -L -s https://raw.githubusercontent.com/teddysun/across/master/bbr.sh)
echo ""
before_show_menu
}
update_shell() {
wget -O /usr/bin/x-ui -N --no-check-certificate https://github.com/sprov065/x-ui/raw/master/x-ui.sh
wget -O /usr/bin/x-ui -N --no-check-certificate https://github.com/vaxilu/x-ui/raw/master/x-ui.sh
if [[ $? != 0 ]]; then
echo ""
echo -e "${red}下载脚本失败,请检查本机能否连接 Github${plain}"
@@ -393,6 +397,7 @@ show_usage() {
echo "x-ui enable - 设置 x-ui 开机自启"
echo "x-ui disable - 取消 x-ui 开机自启"
echo "x-ui log - 查看 x-ui 日志"
echo "x-ui v2-ui - 迁移本机器的 v2-ui 账号数据至 x-ui"
echo "x-ui update - 更新 x-ui 面板"
echo "x-ui install - 安装 x-ui 面板"
echo "x-ui uninstall - 卸载 x-ui 面板"
@@ -402,7 +407,6 @@ show_usage() {
show_menu() {
echo -e "
${green}x-ui 面板管理脚本${plain}
--- https://blog.sprov.xyz/x-ui ---
${green}0.${plain} 退出脚本
————————————————
${green}1.${plain} 安装 x-ui
@@ -480,6 +484,8 @@ if [[ $# > 0 ]]; then
;;
"log") check_install 0 && show_log 0
;;
"v2-ui") check_install 0 && migrate_v2_ui 0
;;
"update") check_install 0 && update 0
;;
"install") check_uninstall 0 && install 0