Compare commits

...

20 Commits

Author SHA1 Message Date
Ho3ein
4ce53920fe v1.3.4 2023-05-01 02:29:24 +03:30
MHSanaei
5100bbba52 simplify ssl cert 2023-04-30 00:57:15 +03:30
MHSanaei
f93d912644 primary type for setDefaultCert 2023-04-29 22:51:33 +03:30
MHSanaei
ce8551b8c4 [darkmode] better colors + add sec to calendar
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-29 22:38:41 +03:30
MHSanaei
d26c21d900 [feature] inbounds auto refresh option
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-29 22:33:45 +03:30
MHSanaei
f5f9347661 Merge branch 'main' of https://github.com/MHSanaei/3x-ui 2023-04-29 22:28:55 +03:30
MHSanaei
a0f5875cb3 [darkmode] fix UTLS - cipherSuites 2023-04-29 22:28:50 +03:30
Ho3ein
3055c68615 Postman Collection thanks to @mehdikhody #303
thanks to @mehdikhody #303
2023-04-29 20:18:24 +03:30
MHSanaei
c3ed8051f3 [feature] add sniffing DestOverride options #298
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-29 18:47:44 +03:30
MHSanaei
d2cdc51c54 [feature] add quic to sniffingObject
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-29 16:26:39 +03:30
MHSanaei
ee896662f5 remove favicon from web root
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-28 18:49:32 +03:30
MHSanaei
177bd036a3 [bug] fix GetClientTrafficByEmail
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-28 18:40:33 +03:30
MHSanaei
d03e049320 v1.3.3 2023-04-28 01:03:59 +03:30
MHSanaei
957d9e24fb Revert "grpc.WithInsecure is deprecated"
This reverts commit 0b896d9c31.
2023-04-28 00:47:56 +03:30
MHSanaei
865e47e9a6 Update check_client_ip_job.go 2023-04-28 00:30:49 +03:30
MHSanaei
607c5d3598 [feature] add grpc multiMode 2023-04-28 00:15:06 +03:30
MHSanaei
8879541999 dark mode - default 2023-04-27 23:48:58 +03:30
MHSanaei
0b896d9c31 grpc.WithInsecure is deprecated 2023-04-27 23:48:22 +03:30
MHSanaei
6f4a2809e2 tls for ss - remove unused 2023-04-27 19:25:48 +03:30
MHSanaei
103a26edb6 [migrate] remove orphaned traffics
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-27 19:05:36 +03:30
27 changed files with 282 additions and 328 deletions

View File

@@ -20,10 +20,10 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
## Install custom version ## Install custom version
To install your desired version you can add the version to the end of install command. Example for ver `v1.3.2`: To install your desired version you can add the version to the end of install command. Example for ver `v1.3.3`:
``` ```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.3.2 bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.3.3
``` ```
# SSL # SSL
@@ -177,6 +177,8 @@ Reference syntax:
| `POST` | `"/resetAllClientTraffics/:id"` | Reset traffics of all clients in an inbound | | `POST` | `"/resetAllClientTraffics/:id"` | Reset traffics of all clients in an inbound |
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) | | `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
- [Postman Collection](https://gist.github.com/mehdikhody/9a862801a2e41f6b5fb6bbc7e1326044)
# A Special Thanks To # A Special Thanks To
- [alireza0](https://github.com/alireza0/) - [alireza0](https://github.com/alireza0/)
@@ -190,11 +192,8 @@ Reference syntax:
# Buy Me a Coffee # Buy Me a Coffee
[![](https://img.shields.io/badge/Wallet-USDT__TRC20-green.svg)](#) - Tron USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
```
TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC
```
# Pictures # Pictures

View File

@@ -1 +1 @@
1.3.2 1.3.4

2
go.sum
View File

@@ -9,8 +9,6 @@ github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI= github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.7 h1:d3sry5vGgVq/OpgozRUNP6xBsSo0mtNdwliApw+SAMQ=
github.com/bytedance/sonic v1.8.7/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.8.8 h1:Kj4AYbZSeENfyXicsYppYKO0K2YWab+i2UTSY7Ukz9Q= github.com/bytedance/sonic v1.8.8 h1:Kj4AYbZSeENfyXicsYppYKO0K2YWab+i2UTSY7Ukz9Q=
github.com/bytedance/sonic v1.8.8/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/bytedance/sonic v1.8.8/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=

View File

@@ -73,7 +73,7 @@ config_after_install() {
/usr/local/x-ui/x-ui migrate /usr/local/x-ui/x-ui migrate
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}" echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
read -p "Do you want to continue with the modification [y/n]? ": config_confirm read -p "Do you want to continue with the modification [y/n]? ": config_confirm
if [[ x"${config_confirm}" == x"y" || x"${config_confirm}" == x"Y" ]]; then if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
read -p "Please set up your username:" config_account read -p "Please set up your username:" config_account
echo -e "${yellow}Your username will be:${config_account}${plain}" echo -e "${yellow}Your username will be:${config_account}${plain}"
read -p "Please set up your password:" config_password read -p "Please set up your password:" config_password

View File

@@ -1,8 +1,9 @@
package logger package logger
import ( import (
"github.com/op/go-logging"
"os" "os"
"github.com/op/go-logging"
) )
var logger *logging.Logger var logger *logging.Logger

View File

@@ -212,6 +212,7 @@
.ant-card-dark .ant-modal-close, .ant-card-dark .ant-modal-close,
.ant-card-dark i, .ant-card-dark i,
.ant-card-dark .ant-select-dropdown-menu-item, .ant-card-dark .ant-select-dropdown-menu-item,
.ant-card-dark .ant-calendar-day-select,
.ant-card-dark .ant-calendar-month-select, .ant-card-dark .ant-calendar-month-select,
.ant-card-dark .ant-calendar-year-select, .ant-card-dark .ant-calendar-year-select,
.ant-card-dark .ant-calendar-date, .ant-card-dark .ant-calendar-date,
@@ -226,7 +227,7 @@
.ant-card-dark .ant-calendar-date:hover, .ant-card-dark .ant-calendar-date:hover,
.ant-card-dark .ant-select-dropdown-menu-item-active, .ant-card-dark .ant-select-dropdown-menu-item-active,
.ant-card-dark li.ant-calendar-time-picker-select-option-selected { .ant-card-dark li.ant-calendar-time-picker-select-option-selected {
background-color: #004488; background-color: #11314d;
} }
.ant-card-dark tbody .ant-table-expanded-row, .ant-card-dark tbody .ant-table-expanded-row,
@@ -243,7 +244,7 @@
.ant-card-dark .ant-select-selection, .ant-card-dark .ant-select-selection,
.ant-card-dark .ant-calendar-picker-clear { .ant-card-dark .ant-calendar-picker-clear {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #2e3b52; background-color: #193752;
} }
.ant-card-dark .ant-select-disabled .ant-select-selection { .ant-card-dark .ant-select-disabled .ant-select-selection {
@@ -264,12 +265,19 @@
.ant-card-dark .ant-modal-content, .ant-card-dark .ant-modal-content,
.ant-card-dark .ant-modal-body, .ant-card-dark .ant-modal-body,
.ant-card-dark .ant-modal-header, .ant-card-dark .ant-modal-header {
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #222a37; background-color: #222a37;
} }
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
background-color: #1668dc;
}
.ant-card-dark .ant-calendar-time-picker-select li:hover {
background: #1668dc;
}
.client-table-header { .client-table-header {
background-color: #f0f2f5; background-color: #f0f2f5;
} }

View File

@@ -28,20 +28,6 @@ const SSMethods = {
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305', BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
}; };
const RULE_IP = {
PRIVATE: 'geoip:private',
CN: 'geoip:cn',
};
const RULE_DOMAIN = {
ADS: 'geosite:category-ads',
ADS_ALL: 'geosite:category-ads-all',
CN: 'geosite:cn',
GOOGLE: 'geosite:google',
FACEBOOK: 'geosite:facebook',
SPEEDTEST: 'geosite:speedtest',
};
const XTLS_FLOW_CONTROL = { const XTLS_FLOW_CONTROL = {
ORIGIN: "xtls-rprx-origin", ORIGIN: "xtls-rprx-origin",
DIRECT: "xtls-rprx-direct", DIRECT: "xtls-rprx-direct",
@@ -93,21 +79,26 @@ const UTLS_FINGERPRINT = {
}; };
const ALPN_OPTION = { const ALPN_OPTION = {
H3: "h3",
H2: "h2",
HTTP1: "http/1.1", HTTP1: "http/1.1",
H2: "h2",
H3: "h3",
};
const SNIFFING_OPTION = {
HTTP: "http",
TLS: "tls",
QUIC: "quic",
}; };
Object.freeze(Protocols); Object.freeze(Protocols);
Object.freeze(VmessMethods); Object.freeze(VmessMethods);
Object.freeze(SSMethods); Object.freeze(SSMethods);
Object.freeze(RULE_IP);
Object.freeze(RULE_DOMAIN);
Object.freeze(XTLS_FLOW_CONTROL); Object.freeze(XTLS_FLOW_CONTROL);
Object.freeze(TLS_FLOW_CONTROL); Object.freeze(TLS_FLOW_CONTROL);
Object.freeze(TLS_VERSION_OPTION); Object.freeze(TLS_VERSION_OPTION);
Object.freeze(TLS_CIPHER_OPTION); Object.freeze(TLS_CIPHER_OPTION);
Object.freeze(ALPN_OPTION); Object.freeze(ALPN_OPTION);
Object.freeze(SNIFFING_OPTION);
class XrayCommonClass { class XrayCommonClass {
@@ -456,18 +447,26 @@ class QuicStreamSettings extends XrayCommonClass {
} }
class GrpcStreamSettings extends XrayCommonClass { class GrpcStreamSettings extends XrayCommonClass {
constructor(serviceName="") { constructor(
serviceName="",
multiMode=false
) {
super(); super();
this.serviceName = serviceName; this.serviceName = serviceName;
this.multiMode = multiMode;
} }
static fromJson(json={}) { static fromJson(json={}) {
return new GrpcStreamSettings(json.serviceName); return new GrpcStreamSettings(
json.serviceName,
json.multiMode
);
} }
toJson() { toJson() {
return { return {
serviceName: this.serviceName, serviceName: this.serviceName,
multiMode: this.multiMode
} }
} }
} }
@@ -888,7 +887,7 @@ class StreamSettings extends XrayCommonClass {
} }
class Sniffing extends XrayCommonClass { class Sniffing extends XrayCommonClass {
constructor(enabled=true, destOverride=['http', 'tls']) { constructor(enabled=true, destOverride=['http', 'tls', 'quic']) {
super(); super();
this.enabled = enabled; this.enabled = enabled;
this.destOverride = destOverride; this.destOverride = destOverride;
@@ -898,7 +897,7 @@ class Sniffing extends XrayCommonClass {
let destOverride = ObjectUtil.clone(json.destOverride); let destOverride = ObjectUtil.clone(json.destOverride);
if (!ObjectUtil.isEmpty(destOverride) && !ObjectUtil.isArrEmpty(destOverride)) { if (!ObjectUtil.isEmpty(destOverride) && !ObjectUtil.isArrEmpty(destOverride)) {
if (ObjectUtil.isEmpty(destOverride[0])) { if (ObjectUtil.isEmpty(destOverride[0])) {
destOverride = ['http', 'tls']; destOverride = ['http', 'tls', 'quic'];
} }
} }
return new Sniffing( return new Sniffing(
@@ -1160,6 +1159,7 @@ class Inbound extends XrayCommonClass {
case Protocols.VMESS: case Protocols.VMESS:
case Protocols.VLESS: case Protocols.VLESS:
case Protocols.TROJAN: case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
break; break;
default: default:
return false; return false;
@@ -1261,50 +1261,6 @@ class Inbound extends XrayCommonClass {
if (this.protocol !== Protocols.VMESS) { if (this.protocol !== Protocols.VMESS) {
return ''; return '';
} }
let network = this.stream.network;
let type = 'none';
let host = '';
let path = '';
if (network === 'tcp') {
let tcp = this.stream.tcp;
type = tcp.type;
if (type === 'http') {
let request = tcp.request;
path = request.path.join(',');
let index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
host = request.headers[index].value;
}
}
} else if (network === 'kcp') {
let kcp = this.stream.kcp;
type = kcp.type;
path = kcp.seed;
} else if (network === 'ws') {
let ws = this.stream.ws;
path = ws.path;
let index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
host = ws.headers[index].value;
}
} else if (network === 'http') {
network = 'h2';
path = this.stream.http.path;
host = this.stream.http.host.join(',');
} else if (network === 'quic') {
type = this.stream.quic.type;
host = this.stream.quic.security;
path = this.stream.quic.key;
} else if (network === 'grpc') {
path = this.stream.grpc.serviceName;
}
if (this.stream.security === 'tls') {
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server;
}
}
let obj = { let obj = {
v: '2', v: '2',
ps: remark, ps: remark,
@@ -1312,16 +1268,66 @@ class Inbound extends XrayCommonClass {
port: this.port, port: this.port,
id: this.settings.vmesses[clientIndex].id, id: this.settings.vmesses[clientIndex].id,
aid: this.settings.vmesses[clientIndex].alterId, aid: this.settings.vmesses[clientIndex].alterId,
net: network, net: this.stream.network,
type: type, type: 'none',
host: host,
path: path,
tls: this.stream.security, tls: this.stream.security,
sni: this.stream.tls.settings.serverName,
fp: this.stream.tls.settings.fingerprint,
alpn: this.stream.tls.alpn.join(','),
allowInsecure: this.stream.tls.settings.allowInsecure,
}; };
let network = this.stream.network;
if (network === 'tcp') {
let tcp = this.stream.tcp;
obj.type = tcp.type;
if (tcp.type === 'http') {
let request = tcp.request;
obj.path = request.path.join(',');
let index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
obj.host = request.headers[index].value;
}
}
} else if (network === 'kcp') {
let kcp = this.stream.kcp;
obj.type = kcp.type;
obj.path = kcp.seed;
} else if (network === 'ws') {
let ws = this.stream.ws;
obj.path = ws.path;
let index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
obj.host = ws.headers[index].value;
}
} else if (network === 'http') {
obj.net = 'h2';
obj.path = this.stream.http.path;
obj.host = this.stream.http.host.join(',');
} else if (network === 'quic') {
obj.type = this.stream.quic.type;
obj.host = this.stream.quic.security;
obj.path = this.stream.quic.key;
} else if (network === 'grpc') {
obj.path = this.stream.grpc.serviceName;
if (this.stream.grpc.multiMode){
obj.type = 'multi'
}
}
if (this.stream.security === 'tls') {
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
obj.add = this.stream.tls.server;
}
if (!ObjectUtil.isEmpty(this.stream.tls.settings.serverName)){
obj.sni = this.stream.tls.settings.serverName;
}
if (!ObjectUtil.isEmpty(this.stream.tls.settings.fingerprint)){
obj.fp = this.stream.tls.settings.fingerprint;
}
if (this.stream.tls.alpn.length>0){
obj.alpn = this.stream.tls.alpn.join(',');
}
if (this.stream.tls.settings.allowInsecure){
obj.allowInsecure = this.stream.tls.settings.allowInsecure;
}
}
return 'vmess://' + base64(JSON.stringify(obj, null, 2)); return 'vmess://' + base64(JSON.stringify(obj, null, 2));
} }
@@ -1374,6 +1380,9 @@ class Inbound extends XrayCommonClass {
case "grpc": case "grpc":
const grpc = this.stream.grpc; const grpc = this.stream.grpc;
params.set("serviceName", grpc.serviceName); params.set("serviceName", grpc.serviceName);
if(grpc.multiMode){
params.set("mode", "multi");
}
break; break;
} }
@@ -1491,6 +1500,9 @@ class Inbound extends XrayCommonClass {
case "grpc": case "grpc":
const grpc = this.stream.grpc; const grpc = this.stream.grpc;
params.set("serviceName", grpc.serviceName); params.set("serviceName", grpc.serviceName);
if(grpc.multiMode){
params.set("mode", "multi");
}
break; break;
} }

View File

@@ -1,9 +1,10 @@
package controller package controller
import ( import (
"github.com/gin-gonic/gin"
"net/http" "net/http"
"x-ui/web/session" "x-ui/web/session"
"github.com/gin-gonic/gin"
) )
type BaseController struct { type BaseController struct {

View File

@@ -2,8 +2,9 @@ package global
import ( import (
"context" "context"
"github.com/robfig/cron/v3"
_ "unsafe" _ "unsafe"
"github.com/robfig/cron/v3"
) )
var webServer WebServer var webServer WebServer

View File

@@ -7,7 +7,8 @@
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css"> <link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css">
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css"> <link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}"> <link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"> <link rel=icon type=”image/x-icon” href="{{ .base_path }}assets/favicon.ico">
<link rel="shortcut icon" type="image/x-icon" href="{{ .base_path }}assets/favicon.ico">
<style> <style>
[v-cloak] { [v-cloak] {
display: none; display: none;

View File

@@ -91,7 +91,7 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker> v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item> </a-form-item>

View File

@@ -66,7 +66,7 @@
const siderDrawer = { const siderDrawer = {
visible: false, visible: false,
collapsed: false, collapsed: false,
isDarkTheme: localStorage.getItem("dark-mode") === 'true' ? true : false, isDarkTheme: localStorage.getItem("dark-mode") === 'false' ? false : true,
show() { show() {
this.visible = true; this.visible = true;
}, },

View File

@@ -120,7 +120,7 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker> v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
<a-tag color="red" v-if="isExpiry">Expired</a-tag> <a-tag color="red" v-if="isExpiry">Expired</a-tag>

View File

@@ -49,7 +49,7 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker> v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item> </a-form-item>

View File

@@ -65,7 +65,7 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker> v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item> </a-form-item>

View File

@@ -71,7 +71,7 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker> v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item> </a-form-item>

View File

@@ -62,7 +62,7 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker> v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item> </a-form-item>

View File

@@ -12,5 +12,10 @@
</span> </span>
<a-switch v-model="inbound.sniffing.enabled"></a-switch> <a-switch v-model="inbound.sniffing.enabled"></a-switch>
</a-form-item> </a-form-item>
<a-form-item>
<a-checkbox-group v-model="inbound.sniffing.destOverride" v-if="inbound.sniffing.enabled">
<a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key">[[ value ]]</a-checkbox>
</a-checkbox-group>
</a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -3,5 +3,8 @@
<a-form-item label="ServiceName"> <a-form-item label="ServiceName">
<a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input> <a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Multi Mode">
<a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
</a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -37,7 +37,7 @@
<a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input> <a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="CipherSuites"> <a-form-item label="CipherSuites">
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px"> <a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="">auto</a-select-option> <a-select-option value="">auto</a-select-option>
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
@@ -56,14 +56,15 @@
<a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input> <a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="uTLS"> <a-form-item label="uTLS">
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 170px"> <a-select v-model="inbound.stream.tls.settings.fingerprint"
style="width: 170px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value=''>None</a-select-option> <a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="Alpn"> <a-form-item label="Alpn">
<a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px"> <a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px">
<a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox> <a-checkbox v-for="key,value in ALPN_OPTION" :value="key">[[ value ]]</a-checkbox>
</a-checkbox-group> </a-checkbox-group>
</a-form-item> </a-form-item>
<a-form-item label="Allow insecure"> <a-form-item label="Allow insecure">
@@ -82,7 +83,7 @@
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'> <a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input> <a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input>
</a-form-item> </a-form-item>
<a-button @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button> <a-button type="primary" icon="import" @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template> </template>
<template v-else> <template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'> <a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
@@ -120,7 +121,7 @@
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'> <a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="inbound.stream.xtls.certs[0].keyFile" style="width:300px;"></a-input> <a-input v-model.trim="inbound.stream.xtls.certs[0].keyFile" style="width:300px;"></a-input>
</a-form-item> </a-form-item>
<a-button @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button> <a-button type="primary" icon="import" @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template> </template>
<template v-else> <template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'> <a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
@@ -142,7 +143,8 @@
<a-input type="number" v-model.number="inbound.stream.reality.xver" :min="0" style="width: 60px"></a-input> <a-input type="number" v-model.number="inbound.stream.reality.xver" :min="0" style="width: 60px"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="uTLS" > <a-form-item label="uTLS" >
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 135px"> <a-select v-model="inbound.stream.reality.settings.fingerprint"
style="width: 135px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>

View File

@@ -41,6 +41,7 @@
<template v-if="inbound.isGrpc"> <template v-if="inbound.isGrpc">
<tr><td>grpc serviceName</td><td><a-tag color="green">[[ inbound.serviceName ]]</a-tag></td></tr> <tr><td>grpc serviceName</td><td><a-tag color="green">[[ inbound.serviceName ]]</a-tag></td></tr>
<tr><td>grpc multiMode</td><td><a-tag color="green">[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr>
</template> </template>
</table> </table>
</td></tr> </td></tr>

View File

@@ -66,28 +66,42 @@
<transition name="list" appear> <transition name="list" appear>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<div slot="title"> <div slot="title">
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button> <a-row>
<a-dropdown :trigger="['click']"> <a-col :xs="24" :sm="24" :lg="12">
<a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button> <a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="siderDrawer.theme"> <a-dropdown :trigger="['click']">
<a-menu-item key="export"> <a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button>
<a-icon type="export"></a-icon> <a-menu slot="overlay" @click="a => generalActions(a)" :theme="siderDrawer.theme">
{{ i18n "pages.inbounds.export" }} <a-menu-item key="export">
</a-menu-item> <a-icon type="export"></a-icon>
<a-menu-item key="resetInbounds"> {{ i18n "pages.inbounds.export" }}
<a-icon type="reload"></a-icon> </a-menu-item>
{{ i18n "pages.inbounds.resetAllTraffic" }} <a-menu-item key="resetInbounds">
</a-menu-item> <a-icon type="reload"></a-icon>
<a-menu-item key="resetClients"> {{ i18n "pages.inbounds.resetAllTraffic" }}
<a-icon type="file-done"></a-icon> </a-menu-item>
{{ i18n "pages.inbounds.resetAllClientTraffics" }} <a-menu-item key="resetClients">
</a-menu-item> <a-icon type="file-done"></a-icon>
<a-menu-item key="delDepletedClients"> {{ i18n "pages.inbounds.resetAllClientTraffics" }}
<a-icon type="rest"></a-icon> </a-menu-item>
{{ i18n "pages.inbounds.delDepletedClients" }} <a-menu-item key="delDepletedClients">
</a-menu-item> <a-icon type="rest"></a-icon>
</a-menu> {{ i18n "pages.inbounds.delDepletedClients" }}
</a-dropdown> </a-menu-item>
</a-menu>
</a-dropdown>
</a-col>
<a-col :xs="24" :sm="24" :lg="12" style="text-align: right;">
<a-select v-model="refreshInterval"
v-if="isRefreshEnabled"
@change="changeRefreshInterval"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
</a-select>
<a-icon type="sync" :spin="isRefreshEnabled"></a-icon>
<a-switch v-model="isRefreshEnabled" @change="toggleRefresh"></a-switch>
</a-col>
</a-row>
</div> </div>
<a-input v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input> <a-input v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id" <a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
@@ -263,7 +277,7 @@
}, { }, {
title: '{{ i18n "pages.inbounds.protocol" }}', title: '{{ i18n "pages.inbounds.protocol" }}',
align: 'left', align: 'left',
width: 80, width: 90,
scopedSlots: { customRender: 'protocol' }, scopedSlots: { customRender: 'protocol' },
}, { }, {
title: '{{ i18n "clients" }}', title: '{{ i18n "clients" }}',
@@ -315,25 +329,22 @@
defaultCert: '', defaultCert: '',
defaultKey: '', defaultKey: '',
clientCount: {}, clientCount: {},
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
}, },
methods: { methods: {
loading(spinning=true) { loading(spinning=true) {
this.spinning = spinning; this.spinning = spinning;
}, },
async getDBInbounds() { async getDBInbounds() {
this.loading();
const msg = await HttpUtil.post('/xui/inbound/list'); const msg = await HttpUtil.post('/xui/inbound/list');
this.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
} }
this.setInbounds(msg.obj); this.setInbounds(msg.obj);
this.searchKey = '';
}, },
async getDefaultSettings() { async getDefaultSettings() {
this.loading();
const msg = await HttpUtil.post('/xui/setting/defaultSettings'); const msg = await HttpUtil.post('/xui/setting/defaultSettings');
this.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
} }
@@ -345,17 +356,16 @@
setInbounds(dbInbounds) { setInbounds(dbInbounds) {
this.inbounds.splice(0); this.inbounds.splice(0);
this.dbInbounds.splice(0); this.dbInbounds.splice(0);
this.searchedInbounds.splice(0);
for (const inbound of dbInbounds) { for (const inbound of dbInbounds) {
const dbInbound = new DBInbound(inbound); const dbInbound = new DBInbound(inbound);
to_inbound = dbInbound.toInbound() to_inbound = dbInbound.toInbound()
this.inbounds.push(to_inbound); this.inbounds.push(to_inbound);
this.dbInbounds.push(dbInbound); this.dbInbounds.push(dbInbound);
this.searchedInbounds.push(dbInbound);
if([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(inbound.protocol) ){ if([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(inbound.protocol) ){
this.clientCount[inbound.id] = this.getClientCounts(inbound,to_inbound); this.clientCount[inbound.id] = this.getClientCounts(inbound,to_inbound);
} }
} }
this.searchInbounds(this.searchKey);
}, },
getClientCounts(dbInbound,inbound){ getClientCounts(dbInbound,inbound){
let clientCount = 0,active = [], deactive = [], depleted = [], expiring = []; let clientCount = 0,active = [], deactive = [], depleted = [], expiring = [];
@@ -788,6 +798,25 @@
} }
txtModal.show('{{ i18n "pages.inbounds.export"}}',copyText,'All-Inbounds'); txtModal.show('{{ i18n "pages.inbounds.export"}}',copyText,'All-Inbounds');
}, },
async startDataRefreshLoop() {
while (this.isRefreshEnabled) {
try {
await this.getDBInbounds();
} catch (e) {
console.error(e);
}
await PromiseUtil.sleep(this.refreshInterval);
}
},
toggleRefresh() {
localStorage.setItem("isRefreshEnabled", this.isRefreshEnabled);
if (this.isRefreshEnabled) {
this.startDataRefreshLoop();
}
},
changeRefreshInterval(){
localStorage.setItem("refreshInterval", this.refreshInterval);
},
}, },
watch: { watch: {
searchKey: debounce(function (newVal) { searchKey: debounce(function (newVal) {
@@ -795,8 +824,15 @@
}, 500) }, 500)
}, },
mounted() { mounted() {
this.loading();
this.getDefaultSettings(); this.getDefaultSettings();
this.getDBInbounds(); if (this.isRefreshEnabled) {
this.startDataRefreshLoop();
}
else {
this.getDBInbounds();
}
this.loading(false);
}, },
computed: { computed: {
total() { total() {
@@ -823,7 +859,6 @@
} }
}, },
}); });
</script> </script>
{{template "inboundModal"}} {{template "inboundModal"}}

View File

@@ -4,23 +4,22 @@ import (
"encoding/json" "encoding/json"
"os" "os"
"regexp" "regexp"
ss "strings"
"x-ui/database" "x-ui/database"
"x-ui/database/model" "x-ui/database/model"
"x-ui/logger" "x-ui/logger"
"x-ui/web/service" "x-ui/web/service"
"x-ui/xray" "x-ui/xray"
// "strconv"
"github.com/go-cmd/cmd"
"net" "net"
"sort" "sort"
"strings" "strings"
"time" "time"
"github.com/go-cmd/cmd"
) )
type CheckClientIpJob struct { type CheckClientIpJob struct {
xrayService service.XrayService xrayService service.XrayService
inboundService service.InboundService
} }
var job *CheckClientIpJob var job *CheckClientIpJob
@@ -36,7 +35,7 @@ func (j *CheckClientIpJob) Run() {
processLogFile() processLogFile()
// disAllowedIps = []string{"192.168.1.183","192.168.1.197"} // disAllowedIps = []string{"192.168.1.183","192.168.1.197"}
blockedIps := []byte(ss.Join(disAllowedIps, ",")) blockedIps := []byte(strings.Join(disAllowedIps, ","))
err := os.WriteFile(xray.GetBlockedIPsPath(), blockedIps, 0755) err := os.WriteFile(xray.GetBlockedIPsPath(), blockedIps, 0755)
checkError(err) checkError(err)
@@ -58,7 +57,7 @@ func processLogFile() {
checkError(err) checkError(err)
} }
lines := ss.Split(string(data), "\n") lines := strings.Split(string(data), "\n")
for _, line := range lines { for _, line := range lines {
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`) ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
emailRegx, _ := regexp.Compile(`email:.+`) emailRegx, _ := regexp.Compile(`email:.+`)
@@ -74,7 +73,7 @@ func processLogFile() {
if matchesEmail == "" { if matchesEmail == "" {
continue continue
} }
matchesEmail = ss.Split(matchesEmail, "email: ")[1] matchesEmail = strings.Split(matchesEmail, "email: ")[1]
if InboundClientIps[matchesEmail] != nil { if InboundClientIps[matchesEmail] != nil {
if contains(InboundClientIps[matchesEmail], ip) { if contains(InboundClientIps[matchesEmail], ip) {
@@ -92,14 +91,12 @@ func processLogFile() {
for clientEmail, ips := range InboundClientIps { for clientEmail, ips := range InboundClientIps {
inboundClientIps, err := GetInboundClientIps(clientEmail) inboundClientIps, err := GetInboundClientIps(clientEmail)
sort.Sort(sort.StringSlice(ips)) sort.Strings(ips)
if err != nil { if err != nil {
addInboundClientIps(clientEmail, ips) addInboundClientIps(clientEmail, ips)
} else { } else {
updateInboundClientIps(inboundClientIps, clientEmail, ips) updateInboundClientIps(inboundClientIps, clientEmail, ips)
} }
} }
// check if inbound connection is more than limited ip and drop connection // check if inbound connection is more than limited ip and drop connection
@@ -202,6 +199,8 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
json.Unmarshal([]byte(inbound.Settings), &settings) json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"] clients := settings["clients"]
var disAllowedIps []string // initialize the slice
for _, client := range clients { for _, client := range clients {
if client.Email == clientEmail { if client.Email == clientEmail {
@@ -214,7 +213,7 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
} }
} }
logger.Debug("disAllowedIps ", disAllowedIps) logger.Debug("disAllowedIps ", disAllowedIps)
sort.Sort(sort.StringSlice(disAllowedIps)) sort.Strings(disAllowedIps)
db := database.GetDB() db := database.GetDB()
err = db.Save(inboundClientIps).Error err = db.Save(inboundClientIps).Error
@@ -223,6 +222,7 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
} }
return nil return nil
} }
func DisableInbound(id int) error { func DisableInbound(id int) error {
db := database.GetDB() db := database.GetDB()
result := db.Model(model.Inbound{}). result := db.Model(model.Inbound{}).

View File

@@ -830,12 +830,14 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl
err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error
if err != nil { if err != nil {
if err == gorm.ErrRecordNotFound { logger.Warning(err)
logger.Warning(err) return nil, err
return nil, err
}
} }
return traffics[0], err if len(traffics) > 0 {
return traffics[0], nil
}
return nil, nil
} }
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) { func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {
@@ -912,6 +914,8 @@ func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error)
func (s *InboundService) MigrationRequirements() { func (s *InboundService) MigrationRequirements() {
db := database.GetDB() db := database.GetDB()
// Fix inbounds based problems
var inbounds []*model.Inbound var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error err := db.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && err != gorm.ErrRecordNotFound {
@@ -922,6 +926,7 @@ func (s *InboundService) MigrationRequirements() {
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings) json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients, ok := settings["clients"].([]interface{}) clients, ok := settings["clients"].([]interface{})
if ok { if ok {
// Fix Clinet configuration problems
var newClients []interface{} var newClients []interface{}
for client_index := range clients { for client_index := range clients {
c := clients[client_index].(map[string]interface{}) c := clients[client_index].(map[string]interface{})
@@ -947,6 +952,7 @@ func (s *InboundService) MigrationRequirements() {
inbounds[inbound_index].Settings = string(modifiedSettings) inbounds[inbound_index].Settings = string(modifiedSettings)
} }
// Add client traffic row for all clients which has email
modelClients, err := s.getClients(inbounds[inbound_index]) modelClients, err := s.getClients(inbounds[inbound_index])
if err != nil { if err != nil {
return return
@@ -962,4 +968,7 @@ func (s *InboundService) MigrationRequirements() {
} }
} }
db.Save(inbounds) db.Save(inbounds)
// Remove orphaned traffics
db.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
} }

View File

@@ -102,80 +102,89 @@ func (s *SubService) getLink(inbound *model.Inbound, email string) string {
} }
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string { func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
address := s.address
if inbound.Protocol != model.VMess { if inbound.Protocol != model.VMess {
return "" return ""
} }
obj := map[string]interface{}{
"v": "2",
"ps": email,
"add": s.address,
"port": inbound.Port,
"type": "none",
}
var stream map[string]interface{} var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream) json.Unmarshal([]byte(inbound.StreamSettings), &stream)
network, _ := stream["network"].(string) network, _ := stream["network"].(string)
typeStr := "none" obj["net"] = network
host := ""
path := ""
sni := ""
fp := ""
var alpn []string
allowInsecure := false
switch network { switch network {
case "tcp": case "tcp":
tcp, _ := stream["tcpSettings"].(map[string]interface{}) tcp, _ := stream["tcpSettings"].(map[string]interface{})
header, _ := tcp["header"].(map[string]interface{}) header, _ := tcp["header"].(map[string]interface{})
typeStr, _ = header["type"].(string) typeStr, _ := header["type"].(string)
obj["type"] = typeStr
if typeStr == "http" { if typeStr == "http" {
request := header["request"].(map[string]interface{}) request := header["request"].(map[string]interface{})
requestPath, _ := request["path"].([]interface{}) requestPath, _ := request["path"].([]interface{})
path = requestPath[0].(string) obj["path"] = requestPath[0].(string)
headers, _ := request["headers"].(map[string]interface{}) headers, _ := request["headers"].(map[string]interface{})
host = searchHost(headers) obj["host"] = searchHost(headers)
} }
case "kcp": case "kcp":
kcp, _ := stream["kcpSettings"].(map[string]interface{}) kcp, _ := stream["kcpSettings"].(map[string]interface{})
header, _ := kcp["header"].(map[string]interface{}) header, _ := kcp["header"].(map[string]interface{})
typeStr, _ = header["type"].(string) obj["type"], _ = header["type"].(string)
path, _ = kcp["seed"].(string) obj["path"], _ = kcp["seed"].(string)
case "ws": case "ws":
ws, _ := stream["wsSettings"].(map[string]interface{}) ws, _ := stream["wsSettings"].(map[string]interface{})
path = ws["path"].(string) obj["path"] = ws["path"].(string)
headers, _ := ws["headers"].(map[string]interface{}) headers, _ := ws["headers"].(map[string]interface{})
host = searchHost(headers) obj["host"] = searchHost(headers)
case "http": case "http":
network = "h2" obj["net"] = "h2"
http, _ := stream["httpSettings"].(map[string]interface{}) http, _ := stream["httpSettings"].(map[string]interface{})
path, _ = http["path"].(string) obj["path"], _ = http["path"].(string)
host = searchHost(http) obj["host"] = searchHost(http)
case "quic": case "quic":
quic, _ := stream["quicSettings"].(map[string]interface{}) quic, _ := stream["quicSettings"].(map[string]interface{})
header := quic["header"].(map[string]interface{}) header := quic["header"].(map[string]interface{})
typeStr, _ = header["type"].(string) obj["type"], _ = header["type"].(string)
host, _ = quic["security"].(string) obj["host"], _ = quic["security"].(string)
path, _ = quic["key"].(string) obj["path"], _ = quic["key"].(string)
case "grpc": case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{}) grpc, _ := stream["grpcSettings"].(map[string]interface{})
path = grpc["serviceName"].(string) obj["path"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
obj["type"] = "multi"
}
} }
security, _ := stream["security"].(string) security, _ := stream["security"].(string)
obj["tls"] = security
if security == "tls" { if security == "tls" {
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{}) tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
alpns, _ := tlsSetting["alpn"].([]interface{}) alpns, _ := tlsSetting["alpn"].([]interface{})
for _, a := range alpns { if len(alpns) > 0 {
alpn = append(alpn, a.(string)) var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
}
obj["alpn"] = strings.Join(alpn, ",")
} }
tlsSettings, _ := searchKey(tlsSetting, "settings") tlsSettings, _ := searchKey(tlsSetting, "settings")
if tlsSetting != nil { if tlsSetting != nil {
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok { if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
sni, _ = sniValue.(string) obj["sni"], _ = sniValue.(string)
} }
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
fp, _ = fpValue.(string) obj["fp"], _ = fpValue.(string)
} }
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok { if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
allowInsecure, _ = insecure.(bool) obj["allowInsecure"], _ = insecure.(bool)
} }
} }
serverName, _ := tlsSetting["serverName"].(string) serverName, _ := tlsSetting["serverName"].(string)
if serverName != "" { if serverName != "" {
address = serverName obj["add"] = serverName
} }
} }
@@ -187,24 +196,9 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
break break
} }
} }
obj["id"] = clients[clientIndex].ID
obj["aid"] = clients[clientIndex].AlterIds
obj := map[string]interface{}{
"v": "2",
"ps": email,
"add": address,
"port": inbound.Port,
"id": clients[clientIndex].ID,
"aid": clients[clientIndex].AlterIds,
"net": network,
"type": typeStr,
"host": host,
"path": path,
"tls": security,
"sni": sni,
"fp": fp,
"alpn": strings.Join(alpn, ","),
"allowInsecure": allowInsecure,
}
jsonStr, _ := json.MarshalIndent(obj, "", " ") jsonStr, _ := json.MarshalIndent(obj, "", " ")
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr) return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
} }
@@ -266,6 +260,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
case "grpc": case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{}) grpc, _ := stream["grpcSettings"].(map[string]interface{})
params["serviceName"] = grpc["serviceName"].(string) params["serviceName"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
params["mode"] = "multi"
}
} }
security, _ := stream["security"].(string) security, _ := stream["security"].(string)
@@ -444,6 +441,9 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
case "grpc": case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{}) grpc, _ := stream["grpcSettings"].(map[string]interface{})
params["serviceName"] = grpc["serviceName"].(string) params["serviceName"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
params["mode"] = "multi"
}
} }
security, _ := stream["security"].(string) security, _ := stream["security"].(string)

View File

@@ -33,9 +33,6 @@ import (
//go:embed assets/* //go:embed assets/*
var assetsFS embed.FS var assetsFS embed.FS
//go:embed assets/favicon.ico
var favicon []byte
//go:embed html/* //go:embed html/*
var htmlFS embed.FS var htmlFS embed.FS
@@ -161,11 +158,6 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine := gin.Default() engine := gin.Default()
// Add favicon
engine.GET("/favicon.ico", func(c *gin.Context) {
c.Data(200, "image/x-icon", favicon)
})
secret, err := s.settingService.GetSecret() secret, err := s.settingService.GetSecret()
if err != nil { if err != nil {
return nil, err return nil, err

128
x-ui.sh
View File

@@ -59,13 +59,13 @@ fi
confirm() { confirm() {
if [[ $# > 1 ]]; then if [[ $# > 1 ]]; then
echo && read -p "$1 [Default $2]: " temp echo && read -p "$1 [Default $2]: " temp
if [[ x"${temp}" == x"" ]]; then if [[ "${temp}" == "" ]]; then
temp=$2 temp=$2
fi fi
else else
read -p "$1 [y/n]: " temp read -p "$1 [y/n]: " temp
fi fi
if [[ x"${temp}" == x"y" || x"${temp}" == x"Y" ]]; then if [[ "${temp}" == "y" || "${temp}" == "Y" ]]; then
return 0 return 0
else else
return 1 return 1
@@ -342,7 +342,7 @@ check_status() {
return 2 return 2
fi fi
temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1) temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1)
if [[ x"${temp}" == x"running" ]]; then if [[ "${temp}" == "running" ]]; then
return 0 return 0
else else
return 1 return 1
@@ -351,7 +351,7 @@ check_status() {
check_enabled() { check_enabled() {
temp=$(systemctl is-enabled x-ui) temp=$(systemctl is-enabled x-ui)
if [[ x"${temp}" == x"enabled" ]]; then if [[ "${temp}" == "enabled" ]]; then
return 0 return 0
else else
return 1 return 1
@@ -431,32 +431,6 @@ show_xray_status() {
fi fi
} }
#this will be an entrance for ssl cert issue
#here we can provide two different methods to issue cert
#first.standalone mode second.DNS API mode
ssl_cert_issue() {
local method=""
echo -E ""
LOGD "********Usage********"
LOGI "this shell script will use acme to help issue certs."
LOGI "here we provide two methods for issuing certs:"
LOGI "method 1:acme standalone mode,need to keep port:80 open"
LOGI "method 2:acme DNS API mode,need provide Cloudflare Global API Key"
LOGI "recommend method 2 first,if it fails,you can try method 1."
LOGI "certs will be installed in /root/cert directory"
read -p "please choose which method do you want,type 1 or 2": method
LOGI "you choosed method:${method}"
if [ "${method}" == "1" ]; then
ssl_cert_issue_standalone
elif [ "${method}" == "2" ]; then
ssl_cert_issue_by_cloudflare
else
LOGE "invalid input,please check it..."
exit 1
fi
}
open_ports() { open_ports() {
if ! command -v ufw &> /dev/null if ! command -v ufw &> /dev/null
then then
@@ -544,7 +518,7 @@ install_acme() {
} }
#method for standalone mode #method for standalone mode
ssl_cert_issue_standalone() { ssl_cert_issue() {
#check for acme.sh first #check for acme.sh first
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
echo "acme.sh could not be found. we will install it" echo "acme.sh could not be found. we will install it"
@@ -555,7 +529,7 @@ ssl_cert_issue_standalone() {
fi fi
fi fi
#install socat second #install socat second
if [[ x"${release}" == x"centos" ]]; then if [[ "${release}" == "centos" ]] || [[ "${release}" == "fedora" ]] ; then
yum install socat -y yum install socat -y
else else
apt install socat -y apt install socat -y
@@ -569,7 +543,7 @@ ssl_cert_issue_standalone() {
#get the domain here,and we need verify it #get the domain here,and we need verify it
local domain="" local domain=""
read -p "please input your domain:" domain read -p "Please enter your domain name:" domain
LOGD "your domain is:${domain},check it..." LOGD "your domain is:${domain},check it..."
#here we need to judge whether there exists cert already #here we need to judge whether there exists cert already
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
@@ -636,94 +610,6 @@ ssl_cert_issue_standalone() {
} }
#method for DNS API mode
ssl_cert_issue_by_cloudflare() {
echo -E ""
LOGD "******Preconditions******"
LOGI "1.need Cloudflare account associated email"
LOGI "2.need Cloudflare Global API Key"
LOGI "3.your domain use Cloudflare as resolver"
confirm "I have confirmed all these info above[y/n]" "y"
if [ $? -eq 0 ]; then
install_acme
if [ $? -ne 0 ]; then
LOGE "install acme failed,please check logs"
exit 1
fi
CF_Domain=""
CF_GlobalKey=""
CF_AccountEmail=""
LOGD "please input your domain:"
read -p "Input your domain here:" CF_Domain
LOGD "your domain is:${CF_Domain},check it..."
#here we need to judge whether there exists cert already
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
if [ ${currentCert} == ${CF_Domain} ]; then
local certInfo=$(~/.acme.sh/acme.sh --list)
LOGE "system already have certs here,can not issue again,current certs details:"
LOGI "$certInfo"
exit 1
else
LOGI "your domain is ready for issuing cert now..."
fi
#create a directory for install cert
certPath="/root/cert/${CF_Domain}"
if [ ! -d "$certPath" ]; then
mkdir -p "$certPath"
else
rm -rf "$certPath"
mkdir -p "$certPath"
fi
LOGD "please inout your cloudflare global API key:"
read -p "Input your key here:" CF_GlobalKey
LOGD "your cloudflare global API key is:${CF_GlobalKey}"
LOGD "please input your cloudflare account email:"
read -p "Input your email here:" CF_AccountEmail
LOGD "your cloudflare account email:${CF_AccountEmail}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
if [ $? -ne 0 ]; then
LOGE "change the default CA to Lets'Encrypt failed,exit"
exit 1
fi
export CF_Key="${CF_GlobalKey}"
export CF_Email=${CF_AccountEmail}
~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} --log
if [ $? -ne 0 ]; then
LOGE "issue cert failed,exit"
rm -rf ~/.acme.sh/${CF_Domain}
exit 1
else
LOGI "Certificate issued Successfully, Installing..."
fi
~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} \
--key-file /root/cert/${CF_Domain}/privkey.pem \
--fullchain-file /root/cert/${CF_Domain}/fullchain.pem
if [ $? -ne 0 ]; then
LOGE "install cert failed,exit"
rm -rf ~/.acme.sh/${CF_Domain}
exit 1
else
LOGI "Certificate installed Successfully,Turning on automatic updates..."
fi
~/.acme.sh/acme.sh --upgrade --auto-upgrade
if [ $? -ne 0 ]; then
LOGE "auto renew failed, certs details:"
ls -lah cert/*
chmod 755 $certPath/*
exit 1
else
LOGI "auto renew succeed, certs details:"
ls -lah cert/*
chmod 755 $certPath/*
fi
else
show_menu
fi
}
warp_fixchatgpt() { warp_fixchatgpt() {
curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash