Updates so far

Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
Alireza Ahmadi
2026-02-01 00:51:53 +01:00
parent d1606d1109
commit 6bab6ce6c4
35 changed files with 1021 additions and 297 deletions

View File

@@ -7,6 +7,7 @@ const Protocols = {
SOCKS: 'socks',
HTTP: 'http',
WIREGUARD: 'wireguard',
TUN: 'tun',
};
const SSMethods = {
@@ -317,7 +318,7 @@ TcpStreamSettings.TcpResponse = class extends XrayCommonClass {
class KcpStreamSettings extends XrayCommonClass {
constructor(
mtu = 1350,
mtu = 1250,
tti = 50,
uplinkCapacity = 5,
downlinkCapacity = 20,
@@ -553,7 +554,6 @@ class TlsStreamSettings extends XrayCommonClass {
maxVersion = TLS_VERSION_OPTION.TLS13,
cipherSuites = '',
rejectUnknownSni = false,
verifyPeerCertInNames = ['dns.google', 'cloudflare-dns.com'],
disableSystemRoot = false,
enableSessionResumption = false,
certificates = [new TlsStreamSettings.Cert()],
@@ -568,7 +568,6 @@ class TlsStreamSettings extends XrayCommonClass {
this.maxVersion = maxVersion;
this.cipherSuites = cipherSuites;
this.rejectUnknownSni = rejectUnknownSni;
this.verifyPeerCertInNames = Array.isArray(verifyPeerCertInNames) ? verifyPeerCertInNames.join(",") : verifyPeerCertInNames;
this.disableSystemRoot = disableSystemRoot;
this.enableSessionResumption = enableSessionResumption;
this.certs = certificates;
@@ -602,7 +601,6 @@ class TlsStreamSettings extends XrayCommonClass {
json.maxVersion,
json.cipherSuites,
json.rejectUnknownSni,
json.verifyPeerCertInNames,
json.disableSystemRoot,
json.enableSessionResumption,
certs,
@@ -620,7 +618,6 @@ class TlsStreamSettings extends XrayCommonClass {
maxVersion: this.maxVersion,
cipherSuites: this.cipherSuites,
rejectUnknownSni: this.rejectUnknownSni,
verifyPeerCertInNames: this.verifyPeerCertInNames.split(","),
disableSystemRoot: this.disableSystemRoot,
enableSessionResumption: this.enableSessionResumption,
certificates: TlsStreamSettings.toJsonArray(this.certs),
@@ -712,20 +709,20 @@ TlsStreamSettings.Settings = class extends XrayCommonClass {
super();
this.allowInsecure = allowInsecure;
this.fingerprint = fingerprint;
this.echConfigList = echConfigList
this.echConfigList = echConfigList;
}
static fromJson(json = {}) {
return new TlsStreamSettings.Settings(
json.allowInsecure,
json.fingerprint,
json.echConfigList
json.echConfigList,
);
}
toJson() {
return {
allowInsecure: this.allowInsecure,
fingerprint: this.fingerprint,
echConfigList: this.echConfigList
echConfigList: this.echConfigList,
};
}
};
@@ -855,6 +852,7 @@ class SockoptStreamSettings extends XrayCommonClass {
V6Only = false,
tcpWindowClamp = 600,
interfaceName = "",
trustedXForwardedFor = [],
) {
super();
this.acceptProxyProtocol = acceptProxyProtocol;
@@ -873,6 +871,7 @@ class SockoptStreamSettings extends XrayCommonClass {
this.V6Only = V6Only;
this.tcpWindowClamp = tcpWindowClamp;
this.interfaceName = interfaceName;
this.trustedXForwardedFor = trustedXForwardedFor;
}
static fromJson(json = {}) {
@@ -894,11 +893,12 @@ class SockoptStreamSettings extends XrayCommonClass {
json.V6Only,
json.tcpWindowClamp,
json.interface,
json.trustedXForwardedFor || [],
);
}
toJson() {
return {
const result = {
acceptProxyProtocol: this.acceptProxyProtocol,
tcpFastOpen: this.tcpFastOpen,
mark: this.mark,
@@ -916,6 +916,10 @@ class SockoptStreamSettings extends XrayCommonClass {
tcpWindowClamp: this.tcpWindowClamp,
interface: this.interfaceName,
};
if (this.trustedXForwardedFor && this.trustedXForwardedFor.length > 0) {
result.trustedXForwardedFor = this.trustedXForwardedFor;
}
return result;
}
}
@@ -1212,6 +1216,13 @@ class Inbound extends XrayCommonClass {
return false;
}
canEnableVisionSeed() {
if (!this.canEnableTlsFlow()) return false;
const clients = this.settings?.vlesses;
if (!Array.isArray(clients)) return false;
return clients.some(c => c?.flow === TLS_FLOW_CONTROL.VISION || c?.flow === TLS_FLOW_CONTROL.VISION_UDP443);
}
canEnableReality() {
if (![Protocols.VLESS, Protocols.TROJAN].includes(this.protocol)) return false;
return ["tcp", "http", "grpc", "xhttp"].includes(this.network);
@@ -1722,6 +1733,7 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.SOCKS: return new Inbound.SocksSettings(protocol);
case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
case Protocols.TUN: return new Inbound.TunSettings(protocol);
default: return null;
}
}
@@ -1736,6 +1748,7 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.SOCKS: return Inbound.SocksSettings.fromJson(json);
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
case Protocols.TUN: return Inbound.TunSettings.fromJson(json);
default: return null;
}
}
@@ -1856,6 +1869,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
encryption = "none",
fallbacks = [],
selectedAuth = undefined,
testseed = [900, 500, 900, 256],
) {
super(protocol);
this.vlesses = vlesses;
@@ -1863,6 +1877,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
this.encryption = encryption;
this.fallbacks = fallbacks;
this.selectedAuth = selectedAuth;
this.testseed = testseed;
}
addFallback() {
@@ -1874,13 +1889,19 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
}
static fromJson(json = {}) {
let testseed = [900, 500, 900, 256];
if (json.testseed && Array.isArray(json.testseed) && json.testseed.length >= 4) {
testseed = json.testseed;
}
const obj = new Inbound.VLESSSettings(
Protocols.VLESS,
(json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
json.decryption,
json.encryption,
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
json.selectedAuth
json.selectedAuth,
testseed
);
return obj;
}
@@ -1906,6 +1927,10 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
json.selectedAuth = this.selectedAuth;
}
if (this.testseed && this.testseed.length >= 4) {
json.testseed = this.testseed;
}
return json;
}
};
@@ -2418,7 +2443,7 @@ Inbound.HttpSettings.HttpAccount = class extends XrayCommonClass {
Inbound.WireguardSettings = class extends XrayCommonClass {
constructor(
protocol,
mtu = 1420,
mtu = 1250,
secretKey = Wireguard.generateKeypair().privateKey,
peers = [new Inbound.WireguardSettings.Peer()],
kernelMode = false
@@ -2498,3 +2523,34 @@ Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
};
}
};
Inbound.TunSettings = class extends Inbound.Settings {
constructor(
protocol,
name = 'xray0',
mtu = 1500,
userLevel = 0
) {
super(protocol);
this.name = name;
this.mtu = mtu;
this.userLevel = userLevel;
}
static fromJson(json = {}) {
return new Inbound.TunSettings(
Protocols.TUN,
json.name ?? 'xray0',
json.mtu ?? json.MTU ?? 1500,
json.userLevel ?? 0
);
}
toJson() {
return {
name: this.name || 'xray0',
mtu: this.mtu || 1500,
userLevel: this.userLevel || 0,
};
}
};

View File

@@ -8,7 +8,8 @@ const Protocols = {
Shadowsocks: "shadowsocks",
Socks: "socks",
HTTP: "http",
Wireguard: "wireguard"
Wireguard: "wireguard",
Hysteria: "hysteria"
};
const SSMethods = {
@@ -162,7 +163,7 @@ class TcpStreamSettings extends CommonClass {
class KcpStreamSettings extends CommonClass {
constructor(
mtu = 1350,
mtu = 1250,
tti = 50,
uplinkCapacity = 5,
downlinkCapacity = 20,
@@ -354,6 +355,7 @@ class TlsStreamSettings extends CommonClass {
fingerprint = '',
allowInsecure = false,
echConfigList = '',
verifyPeerCertByName = []
) {
super();
this.serverName = serverName;
@@ -361,6 +363,7 @@ class TlsStreamSettings extends CommonClass {
this.fingerprint = fingerprint;
this.allowInsecure = allowInsecure;
this.echConfigList = echConfigList;
this.verifyPeerCertByName = Array.isArray(verifyPeerCertByName) ? verifyPeerCertByName.join(",") : verifyPeerCertByName;
}
static fromJson(json = {}) {
@@ -370,6 +373,7 @@ class TlsStreamSettings extends CommonClass {
json.fingerprint,
json.allowInsecure,
json.echConfigList,
json.verifyPeerCertByName
);
}
@@ -380,6 +384,7 @@ class TlsStreamSettings extends CommonClass {
fingerprint: this.fingerprint,
allowInsecure: this.allowInsecure,
echConfigList: this.echConfigList,
verifyPeerCertByName: this.verifyPeerCertByName.length > 0 ? this.verifyPeerCertByName.split(",") : undefined,
};
}
}
@@ -422,6 +427,91 @@ class RealityStreamSettings extends CommonClass {
};
}
};
class HysteriaStreamSettings extends CommonClass {
constructor(
version = 2,
auth = '',
congestion = '',
up = '0',
down = '0',
udphopPort = '',
udphopInterval = 30,
initStreamReceiveWindow = 8388608,
maxStreamReceiveWindow = 8388608,
initConnectionReceiveWindow = 20971520,
maxConnectionReceiveWindow = 20971520,
maxIdleTimeout = 30,
keepAlivePeriod = 0,
disablePathMTUDiscovery = false
) {
super();
this.version = version;
this.auth = auth;
this.congestion = congestion;
this.up = up;
this.down = down;
this.udphopPort = udphopPort;
this.udphopInterval = udphopInterval;
this.initStreamReceiveWindow = initStreamReceiveWindow;
this.maxStreamReceiveWindow = maxStreamReceiveWindow;
this.initConnectionReceiveWindow = initConnectionReceiveWindow;
this.maxConnectionReceiveWindow = maxConnectionReceiveWindow;
this.maxIdleTimeout = maxIdleTimeout;
this.keepAlivePeriod = keepAlivePeriod;
this.disablePathMTUDiscovery = disablePathMTUDiscovery;
}
static fromJson(json = {}) {
let udphopPort = '';
let udphopInterval = 30;
if (json.udphop) {
udphopPort = json.udphop.port || '';
udphopInterval = json.udphop.interval || 30;
}
return new HysteriaStreamSettings(
json.version,
json.auth,
json.congestion,
json.up,
json.down,
udphopPort,
udphopInterval,
json.initStreamReceiveWindow,
json.maxStreamReceiveWindow,
json.initConnectionReceiveWindow,
json.maxConnectionReceiveWindow,
json.maxIdleTimeout,
json.keepAlivePeriod,
json.disablePathMTUDiscovery
);
}
toJson() {
const result = {
version: this.version,
auth: this.auth,
congestion: this.congestion,
up: this.up,
down: this.down,
initStreamReceiveWindow: this.initStreamReceiveWindow,
maxStreamReceiveWindow: this.maxStreamReceiveWindow,
initConnectionReceiveWindow: this.initConnectionReceiveWindow,
maxConnectionReceiveWindow: this.maxConnectionReceiveWindow,
maxIdleTimeout: this.maxIdleTimeout,
keepAlivePeriod: this.keepAlivePeriod,
disablePathMTUDiscovery: this.disablePathMTUDiscovery
};
if (this.udphopPort) {
result.udphop = {
port: this.udphopPort,
interval: this.udphopInterval
};
}
return result;
}
};
class SockoptStreamSettings extends CommonClass {
constructor(
dialerProxy = "",
@@ -430,6 +520,7 @@ class SockoptStreamSettings extends CommonClass {
tcpMptcp = false,
penetrate = false,
addressPortStrategy = Address_Port_Strategy.NONE,
trustedXForwardedFor = [],
) {
super();
this.dialerProxy = dialerProxy;
@@ -438,6 +529,7 @@ class SockoptStreamSettings extends CommonClass {
this.tcpMptcp = tcpMptcp;
this.penetrate = penetrate;
this.addressPortStrategy = addressPortStrategy;
this.trustedXForwardedFor = trustedXForwardedFor;
}
static fromJson(json = {}) {
@@ -448,12 +540,13 @@ class SockoptStreamSettings extends CommonClass {
json.tcpKeepAliveInterval,
json.tcpMptcp,
json.penetrate,
json.addressPortStrategy
json.addressPortStrategy,
json.trustedXForwardedFor || []
);
}
toJson() {
return {
const result = {
dialerProxy: this.dialerProxy,
tcpFastOpen: this.tcpFastOpen,
tcpKeepAliveInterval: this.tcpKeepAliveInterval,
@@ -461,6 +554,34 @@ class SockoptStreamSettings extends CommonClass {
penetrate: this.penetrate,
addressPortStrategy: this.addressPortStrategy
};
if (this.trustedXForwardedFor && this.trustedXForwardedFor.length > 0) {
result.trustedXForwardedFor = this.trustedXForwardedFor;
}
return result;
}
}
class UdpMask extends CommonClass {
constructor(type = 'salamander', password = '') {
super();
this.type = type;
this.password = password;
}
static fromJson(json = {}) {
return new UdpMask(
json.type,
json.settings?.password || ''
);
}
toJson() {
return {
type: this.type,
settings: {
password: this.password
}
};
}
}
@@ -476,6 +597,8 @@ class StreamSettings extends CommonClass {
grpcSettings = new GrpcStreamSettings(),
httpupgradeSettings = new HttpUpgradeStreamSettings(),
xhttpSettings = new xHTTPStreamSettings(),
hysteriaSettings = new HysteriaStreamSettings(),
udpmasks = [],
sockopt = undefined,
) {
super();
@@ -489,9 +612,19 @@ class StreamSettings extends CommonClass {
this.grpc = grpcSettings;
this.httpupgrade = httpupgradeSettings;
this.xhttp = xhttpSettings;
this.hysteria = hysteriaSettings;
this.udpmasks = udpmasks;
this.sockopt = sockopt;
}
addUdpMask() {
this.udpmasks.push(new UdpMask());
}
delUdpMask(index) {
this.udpmasks.splice(index, 1);
}
get isTls() {
return this.security === 'tls';
}
@@ -520,6 +653,8 @@ class StreamSettings extends CommonClass {
GrpcStreamSettings.fromJson(json.grpcSettings),
HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
xHTTPStreamSettings.fromJson(json.xhttpSettings),
HysteriaStreamSettings.fromJson(json.hysteriaSettings),
json.udpmasks ? json.udpmasks.map(mask => UdpMask.fromJson(mask)) : [],
SockoptStreamSettings.fromJson(json.sockopt),
);
}
@@ -537,6 +672,8 @@ class StreamSettings extends CommonClass {
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,
hysteriaSettings: network === 'hysteria' ? this.hysteria.toJson() : undefined,
udpmasks: this.udpmasks.length > 0 ? this.udpmasks.map(mask => mask.toJson()) : undefined,
sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
};
}
@@ -596,11 +733,12 @@ class Outbound extends CommonClass {
set protocol(protocol) {
this._protocol = protocol;
this.settings = Outbound.Settings.getSettings(protocol);
this.stream = new StreamSettings();
this.stream = new StreamSettings(protocol === Protocols.Hysteria ? 'hysteria' : 'tcp');
}
canEnableTls() {
if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol)) return false;
if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.Hysteria].includes(this.protocol)) return false;
if (this.protocol === Protocols.Hysteria) return this.stream.network === 'hysteria';
return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.stream.network);
}
@@ -612,13 +750,19 @@ class Outbound extends CommonClass {
return false;
}
canEnableVisionSeed() {
if (!this.canEnableTlsFlow()) return false;
const flow = this.settings?.flow;
return flow === TLS_FLOW_CONTROL.VISION || flow === TLS_FLOW_CONTROL.VISION_UDP443;
}
canEnableReality() {
if (![Protocols.VLESS, Protocols.Trojan].includes(this.protocol)) return false;
return ["tcp", "http", "grpc", "xhttp"].includes(this.stream.network);
}
canEnableStream() {
return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol);
return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.Hysteria].includes(this.protocol);
}
canEnableMux() {
@@ -645,10 +789,6 @@ class Outbound extends CommonClass {
].includes(this.protocol);
}
hasVnext() {
return [Protocols.VMess, Protocols.VLESS].includes(this.protocol);
}
hasServers() {
return [Protocols.Trojan, Protocols.Shadowsocks, Protocols.Socks, Protocols.HTTP].includes(this.protocol);
}
@@ -661,7 +801,8 @@ class Outbound extends CommonClass {
Protocols.Trojan,
Protocols.Shadowsocks,
Protocols.Socks,
Protocols.HTTP
Protocols.HTTP,
Protocols.Hysteria
].includes(this.protocol);
}
@@ -688,13 +829,19 @@ class Outbound extends CommonClass {
if (this.stream?.sockopt)
stream = { sockopt: this.stream.sockopt.toJson() };
}
let settingsOut = this.settings instanceof CommonClass ? this.settings.toJson() : this.settings;
if (settingsOut && typeof settingsOut === 'object') {
Object.keys(settingsOut).forEach(k => {
if (settingsOut[k] === undefined || settingsOut[k] === null) delete settingsOut[k];
});
}
return {
tag: this.tag == '' ? undefined : this.tag,
protocol: this.protocol,
settings: this.settings instanceof CommonClass ? this.settings.toJson() : this.settings,
streamSettings: stream,
sendThrough: this.sendThrough != "" ? this.sendThrough : undefined,
mux: this.mux?.enabled ? this.mux : undefined,
settings: settingsOut,
...(this.tag ? { tag: this.tag } : {}),
...(stream ? { streamSettings: stream } : {}),
...(this.sendThrough ? { sendThrough: this.sendThrough } : {}),
...(this.mux?.enabled ? { mux: this.mux } : {}),
};
}
@@ -708,6 +855,9 @@ class Outbound extends CommonClass {
case Protocols.Trojan:
case 'ss':
return this.fromParamLink(link);
case 'hysteria2':
case Protocols.Hysteria:
return this.fromHysteriaLink(link);
default:
return null;
}
@@ -828,6 +978,65 @@ class Outbound extends CommonClass {
remark = remark.length > 0 ? remark.substring(1) : 'out-' + protocol + '-' + port;
return new Outbound(remark, protocol, settings, stream);
}
static fromHysteriaLink(link) {
const regex = /^hysteria2?:\/\/([^@]+)@([^:?#]+):(\d+)([^#]*)(#.*)?$/;
const match = link.match(regex);
if (!match) return null;
let [, password, address, port, params, hash] = match;
port = parseInt(port);
let urlParams = new URLSearchParams(params);
let stream = new StreamSettings('hysteria', 'none');
stream.hysteria.auth = password;
stream.hysteria.congestion = urlParams.get('congestion') ?? '';
stream.hysteria.up = urlParams.get('up') ?? '0';
stream.hysteria.down = urlParams.get('down') ?? '0';
stream.hysteria.udphopPort = urlParams.get('mport') ?? urlParams.get('udphopPort') ?? '';
stream.hysteria.udphopInterval = parseInt(urlParams.get('udphopInterval') ?? '30');
if (urlParams.has('initStreamReceiveWindow')) {
stream.hysteria.initStreamReceiveWindow = parseInt(urlParams.get('initStreamReceiveWindow'));
}
if (urlParams.has('maxStreamReceiveWindow')) {
stream.hysteria.maxStreamReceiveWindow = parseInt(urlParams.get('maxStreamReceiveWindow'));
}
if (urlParams.has('initConnectionReceiveWindow')) {
stream.hysteria.initConnectionReceiveWindow = parseInt(urlParams.get('initConnectionReceiveWindow'));
}
if (urlParams.has('maxConnectionReceiveWindow')) {
stream.hysteria.maxConnectionReceiveWindow = parseInt(urlParams.get('maxConnectionReceiveWindow'));
}
if (urlParams.has('maxIdleTimeout')) {
stream.hysteria.maxIdleTimeout = parseInt(urlParams.get('maxIdleTimeout'));
}
if (urlParams.has('keepAlivePeriod')) {
stream.hysteria.keepAlivePeriod = parseInt(urlParams.get('keepAlivePeriod'));
}
if (urlParams.has('disablePathMTUDiscovery')) {
stream.hysteria.disablePathMTUDiscovery = urlParams.get('disablePathMTUDiscovery') === 'true';
}
if (urlParams.has('obfs')) {
stream.udpmasks[0] = new UdpMask(urlParams.get('obfs'), urlParams.get('obfs-password'));
}
if (urlParams.has('security')){
stream.security = urlParams.get('security');
stream.tls = new TlsStreamSettings(
urlParams.get('sni'),
urlParams.get('alpn') ? urlParams.get('alpn').split(',') : [],
urlParams.get('fp') ?? undefined,
urlParams.get('insecure') ?? urlParams.get('allowInsecure') ?? false,
);
}
let settings = new Outbound.HysteriaSettings(address, port, 2);
let remark = hash ? decodeURIComponent(hash.substring(1)) : `out-hysteria-${port}`;
return new Outbound(remark, Protocols.Hysteria, settings, stream);
}
}
Outbound.Settings = class extends CommonClass {
@@ -848,6 +1057,7 @@ Outbound.Settings = class extends CommonClass {
case Protocols.Socks: return new Outbound.SocksSettings();
case Protocols.HTTP: return new Outbound.HttpSettings();
case Protocols.Wireguard: return new Outbound.WireguardSettings();
case Protocols.Hysteria: return new Outbound.HysteriaSettings();
default: return null;
}
}
@@ -864,6 +1074,7 @@ Outbound.Settings = class extends CommonClass {
case Protocols.Socks: return Outbound.SocksSettings.fromJson(json);
case Protocols.HTTP: return Outbound.HttpSettings.fromJson(json);
case Protocols.Wireguard: return Outbound.WireguardSettings.fromJson(json);
case Protocols.Hysteria: return Outbound.HysteriaSettings.fromJson(json);
default: return null;
}
}
@@ -1017,54 +1228,64 @@ Outbound.VmessSettings = class extends CommonClass {
}
static fromJson(json = {}) {
if (ObjectUtil.isArrEmpty(json.vnext)) return new Outbound.VmessSettings();
if (ObjectUtil.isEmpty(json.address) || ObjectUtil.isEmpty(json.port)) return new Outbound.VmessSettings();
return new Outbound.VmessSettings(
json.vnext[0].address,
json.vnext[0].port,
json.vnext[0].users[0].id,
json.vnext[0].users[0].security,
json.address,
json.port,
json.id,
json.security,
);
}
toJson() {
return {
vnext: [{
address: this.address,
port: this.port,
users: [{ id: this.id, security: this.security }],
}],
address: this.address,
port: this.port,
id: this.id,
security: this.security,
};
}
};
Outbound.VLESSSettings = class extends CommonClass {
constructor(address, port, id, flow, encryption) {
constructor(address, port, id, flow, encryption, testpre = 0, testseed = [900, 500, 900, 256]) {
super();
this.address = address;
this.port = port;
this.id = id;
this.flow = flow;
this.encryption = encryption;
this.testpre = testpre;
this.testseed = testseed;
}
static fromJson(json = {}) {
if (ObjectUtil.isArrEmpty(json.vnext)) return new Outbound.VLESSSettings();
if (ObjectUtil.isEmpty(json.address) || ObjectUtil.isEmpty(json.port)) return new Outbound.VLESSSettings();
return new Outbound.VLESSSettings(
json.vnext[0].address,
json.vnext[0].port,
json.vnext[0].users[0].id,
json.vnext[0].users[0].flow,
json.vnext[0].users[0].encryption,
json.address,
json.port,
json.id,
json.flow,
json.encryption,
json.testpre || 0,
json.testseed && json.testseed.length >= 4 ? json.testseed : [900, 500, 900, 256]
);
}
toJson() {
return {
vnext: [{
address: this.address,
port: this.port,
users: [{ id: this.id, flow: this.flow, encryption: this.encryption }],
}],
const result = {
address: this.address,
port: this.port,
id: this.id,
flow: this.flow,
encryption: this.encryption,
};
if (this.testpre > 0) {
result.testpre = this.testpre;
}
if (this.testseed && this.testseed.length >= 4) {
result.testseed = this.testseed;
}
return result;
}
};
Outbound.TrojanSettings = class extends CommonClass {
@@ -1195,7 +1416,7 @@ Outbound.HttpSettings = class extends CommonClass {
Outbound.WireguardSettings = class extends CommonClass {
constructor(
mtu = 1420,
mtu = 1250,
secretKey = '',
address = [''],
workers = 2,
@@ -1286,4 +1507,30 @@ Outbound.WireguardSettings.Peer = class extends CommonClass {
keepAlive: this.keepAlive ?? undefined,
};
}
};
Outbound.HysteriaSettings = class extends CommonClass {
constructor(address = '', port = 443, version = 2) {
super();
this.address = address;
this.port = port;
this.version = version;
}
static fromJson(json = {}) {
if (Object.keys(json).length === 0) return new Outbound.HysteriaSettings();
return new Outbound.HysteriaSettings(
json.address,
json.port,
json.version
);
}
toJson() {
return {
address: this.address,
port: this.port,
version: this.version
};
}
};

View File

@@ -28,7 +28,7 @@
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
<a-input-number v-model.number="inbound.port"></a-input-number>
<a-input-number v-model.number="inbound.port" :min="1" :max="65535"></a-input-number>
</a-form-item>
<a-form-item>
@@ -100,6 +100,11 @@
{{template "form/wireguard"}}
</template>
<!-- tun -->
<template v-if="inbound.protocol === Protocols.TUN">
{{template "form/tun"}}
</template>
<!-- stream settings -->
<template v-if="inbound.canEnableStream()">
{{template "form/streamSettings"}}

View File

@@ -205,11 +205,21 @@
</a-form-item>
</template>
<!-- Vnext (vless/vmess) settings -->
<!-- vless/vmess user settings -->
<template v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)">
<a-form-item label='ID'>
<a-input v-model.trim="outbound.settings.id"></a-input>
<a-form-item label='ID'>
<a-input v-model.trim="outbound.settings.id"></a-input>
</a-form-item>
<!-- vmess settings -->
<template v-if="outbound.protocol === Protocols.VMess">
<a-form-item label='Security'>
<a-select v-model="outbound.settings.security" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</template>
<!-- vless settings -->
<template v-if="outbound.protocol === Protocols.VLESS">
<a-form-item label='encryption'>
@@ -224,6 +234,28 @@
</a-select>
</a-form-item>
</template>
<!-- XTLS Vision Advanced Settings -->
<template v-if="outbound.canEnableVisionSeed()">
<a-form-item label="Vision Pre-Connect">
<a-input-number v-model.number="outbound.settings.testpre" :min="0" :max="10" :style="{ width: '100%' }" placeholder="0"></a-input-number>
</a-form-item>
<a-form-item label="Vision Seed">
<a-row :gutter="8">
<a-col :span="6">
<a-input-number v-model.number="outbound.settings.testseed[0]" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900" addon-before="[0]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number v-model.number="outbound.settings.testseed[1]" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="500" addon-before="[1]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number v-model.number="outbound.settings.testseed[2]" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900" addon-before="[2]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number v-model.number="outbound.settings.testseed[3]" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="256" addon-before="[3]"></a-input-number>
</a-col>
</a-row>
</a-form-item>
</template>
</template>
<!-- Servers (trojan/shadowsocks/socks/http) settings -->
@@ -253,7 +285,14 @@
<a-form-item label='UDP over TCP'>
<a-switch v-model="outbound.settings.uot"></a-switch>
</a-form-item>
</template>
</template>
<!-- hysteria settings -->
<template v-if="outbound.protocol === Protocols.Hysteria">
<a-form-item label='Version'>
<a-input-number v-model.number="outbound.settings.version" :min="2"
:max="2" disabled></a-input-number>
</a-form-item>
</template>
</template>
<!-- stream settings -->
@@ -261,12 +300,17 @@
<a-form-item label='{{ i18n "transmission" }}'>
<a-select v-model="outbound.stream.network" @change="streamNetworkChange"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="kcp">mKCP</a-select-option>
<a-select-option value="ws">WebSocket</a-select-option>
<a-select-option value="grpc">gRPC</a-select-option>
<a-select-option value="httpupgrade">HTTPUpgrade</a-select-option>
<a-select-option value="xhttp">XHTTP</a-select-option>
<template v-if="outbound.protocol != Protocols.Hysteria">
<a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="kcp">mKCP</a-select-option>
<a-select-option value="ws">WebSocket</a-select-option>
<a-select-option value="grpc">gRPC</a-select-option>
<a-select-option value="httpupgrade">HTTPUpgrade</a-select-option>
<a-select-option value="xhttp">XHTTP</a-select-option>
</template>
<template v-else>
<a-select-option value="hysteria">Hysteria2</a-select-option>
</template>
</a-select>
</a-form-item>
<template v-if="outbound.stream.network === 'tcp'">
@@ -396,6 +440,90 @@
<a-input-number v-model.number="outbound.stream.xhttp.xmux.hKeepAlivePeriod"></a-input-number>
</a-form-item>
</template>
<!-- hysteria -->
<template v-if="outbound.stream.network === 'hysteria'">
<a-form-item label='Auth Password'>
<a-input v-model.trim="outbound.stream.hysteria.auth"></a-input>
</a-form-item>
<a-form-item label='Congestion'>
<a-select v-model="outbound.stream.hysteria.congestion" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="">BBR (Auto)</a-select-option>
<a-select-option value="brutal">Brutal</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Upload Speed'>
<a-input v-model.trim="outbound.stream.hysteria.up"
placeholder="0 (BBR mode), e.g., 100 mbps"></a-input>
</a-form-item>
<a-form-item label='Download Speed'>
<a-input v-model.trim="outbound.stream.hysteria.down"
placeholder="0 (BBR mode), e.g., 100 mbps"></a-input>
</a-form-item>
<a-form-item label='UDP Hop Port'>
<a-input v-model.trim="outbound.stream.hysteria.udphopPort"
placeholder="e.g., 1145-1919 or 11,13,15-17"></a-input>
</a-form-item>
<a-form-item label='UDP Hop Interval (s)'
v-if="outbound.stream.hysteria.udphopPort">
<a-input-number
v-model.number="outbound.stream.hysteria.udphopInterval"
:min="5"></a-input-number>
</a-form-item>
<a-form-item label='Init Stream Receive'>
<a-input-number
v-model.number="outbound.stream.hysteria.initStreamReceiveWindow"></a-input-number>
</a-form-item>
<a-form-item label='Max Stream Receive'>
<a-input-number
v-model.number="outbound.stream.hysteria.maxStreamReceiveWindow"></a-input-number>
</a-form-item>
<a-form-item label='Init Connection Receive'>
<a-input-number
v-model.number="outbound.stream.hysteria.initConnectionReceiveWindow"></a-input-number>
</a-form-item>
<a-form-item label='Max Connection Receive'>
<a-input-number
v-model.number="outbound.stream.hysteria.maxConnectionReceiveWindow"></a-input-number>
</a-form-item>
<a-form-item label='Max Idle Timeout (s)'>
<a-input-number
v-model.number="outbound.stream.hysteria.maxIdleTimeout" :min="4"
:max="120"></a-input-number>
</a-form-item>
<a-form-item label='Keep Alive Period (s)'>
<a-input-number
v-model.number="outbound.stream.hysteria.keepAlivePeriod" :min="0"
:max="60"></a-input-number>
</a-form-item>
<a-form-item label='Disable Path MTU'>
<a-switch
v-model="outbound.stream.hysteria.disablePathMTUDiscovery"></a-switch>
</a-form-item>
</template>
</template>
<!-- udpmasks settings -->
<template v-if="outbound.canEnableStream()">
<a-form-item label="UDP Masks">
<a-button icon="plus" type="primary" size="small" @click="outbound.stream.addUdpMask()"></a-button>
</a-form-item>
<template v-if="outbound.stream.udpmasks.length > 0">
<a-form v-for="(mask, index) in outbound.stream.udpmasks" :key="index" :colon="false"
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '0' }"> UDP Mask [[ index + 1 ]]
<a-icon type="delete" @click="() => outbound.stream.delUdpMask(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
<a-form-item label='Type'>
<a-select v-model="mask.type" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="salamander">Salamander</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Password'>
<a-input v-model.trim="mask.password" placeholder="Obfuscation password"></a-input>
</a-form-item>
</a-form>
</template>
</template>
<!-- tls settings -->
@@ -433,6 +561,9 @@
<a-form-item label="Allow Insecure">
<a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch>
</a-form-item>
<a-form-item label="VerifyPeerCertByNames">
<a-input v-model.trim="outbound.stream.tls.verifyPeerCertByNames"></a-input>
</a-form-item>
</template>
<!-- reality settings -->
@@ -488,6 +619,15 @@
<a-form-item label="Penetrate">
<a-switch v-model="outbound.stream.sockopt.penetrate"></a-switch>
</a-form-item>
<a-form-item label="Trusted X-Forwarded-For">
<a-select mode="tags" v-model="outbound.stream.sockopt.trustedXForwardedFor" :style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
<a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
<a-select-option value="True-Client-IP">True-Client-IP</a-select-option>
<a-select-option value="X-Client-IP">X-Client-IP</a-select-option>
</a-select>
</a-form-item>
</template>
<!-- mux settings -->
@@ -514,7 +654,7 @@
</a-tab-pane>
<a-tab-pane key="2" tab="JSON" force-render="true">
<a-form-item style="margin: 10px 0">
Link: <a-input v-model.trim="outModal.link" style="width: 300px; margin-right: 5px;" placeholder="vmess:// vless:// trojan:// ss://"></a-input>
Link: <a-input v-model.trim="outModal.link" style="width: 300px; margin-right: 5px;" placeholder="vmess:// vless:// trojan:// ss:// hysteria2://"></a-input>
<a-button @click="convertLink" type="primary"><a-icon type="form"></a-icon></a-button>
</a-form-item>
<textarea style="position:absolute; left: -800px;" id="outboundJson"></textarea>

View File

@@ -0,0 +1,44 @@
{{define "form/tun"}}
<a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.tun.nameDesc" }}</span>
</template>
Interface Name
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.settings.name"
placeholder="xray0"></a-input>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.tun.mtuDesc" }}</span>
</template>
MTU
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number v-model.number="inbound.settings.mtu" :min="1"
:max="9000" placeholder="1500"></a-input-number>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.tun.userLevelDesc" }}</span>
</template>
{{ i18n "pages.xray.tun.userLevel" }}
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number v-model.number="inbound.settings.userLevel" :min="0"
placeholder="0"></a-input-number>
</a-form-item>
</a-form>
{{end}}

View File

@@ -75,4 +75,33 @@
</a-form>
<a-divider style="margin:5px 0;"></a-divider>
</template>
<template v-if="inbound.canEnableVisionSeed()">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Vision Seed">
<a-row :gutter="8">
<a-col :span="6">
<a-input-number :value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900" @change="(val) => updateTestseed(0, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900" addon-before="[0]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number :value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500" @change="(val) => updateTestseed(1, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="500" addon-before="[1]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number :value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900" @change="(val) => updateTestseed(2, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900" addon-before="[2]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number :value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256" @change="(val) => updateTestseed(3, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="256" addon-before="[3]"></a-input-number>
</a-col>
</a-row>
<a-space :size="8" :style="{ marginTop: '8px' }">
<a-button type="primary" @click="setRandomTestseed">
Rand
</a-button>
<a-button @click="resetTestseed">
Reset
</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider>
</template>
{{end}}

View File

@@ -17,10 +17,10 @@
</template>
<a-input style="width: 35%; border-radius: 0;" v-model.trim="row.dest" placeholder='{{ i18n "host" }}'></a-input>
<a-tooltip title='{{ i18n "pages.inbounds.port" }}'>
<a-input-number style="width: 15%;" v-model.number="row.port" min="1" max="65531"></a-input-number>
<a-input-number style="width: 15%;" v-model.number="row.port" min="1" max="65535"></a-input-number>
</a-tooltip>
<a-input style="width: 20%; border-radius: 0;" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'></a-input>
<a-button style="width: 10%; margin: 0px" @click="inbound.stream.externalProxy.splice(index, 1)">-</a-button>
</a-input-group>
</a-form>
{{end}}
{{end}}

View File

@@ -61,6 +61,15 @@
<a-form-item label="Interface Name">
<a-input v-model="inbound.stream.sockopt.interfaceName"></a-input>
</a-form-item>
<a-form-item label="Trusted X-Forwarded-For">
<a-select mode="tags" v-model="inbound.stream.sockopt.trustedXForwardedFor" :style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
<a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
<a-select-option value="True-Client-IP">True-Client-IP</a-select-option>
<a-select-option value="X-Client-IP">X-Client-IP</a-select-option>
</a-select>
</a-form-item>
</template>
</a-form>
{{end}}

View File

@@ -52,6 +52,13 @@
<a-form-item label="Reject Unknown SNI">
<a-switch v-model="inbound.stream.tls.rejectUnknownSni"></a-switch>
</a-form-item>
<a-form-item label="Disable System Root">
<a-switch v-model="inbound.stream.tls.disableSystemRoot"></a-switch>
</a-form-item>
<a-form-item label="Session Resumption">
<a-switch v-model="inbound.stream.tls.enableSessionResumption"></a-switch>
</a-form-item>
<a-divider :style="{ margin: '0' }"></a-divider>
<template v-for="cert,index in inbound.stream.tls.certs">
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid">

View File

@@ -26,6 +26,11 @@
} else {
this.inbound = new Inbound();
}
if (this.inbound.protocol === Protocols.VLESS && this.inbound.settings) {
if (!this.inbound.settings.testseed || !Array.isArray(this.inbound.settings.testseed) || this.inbound.settings.testseed.length < 4) {
this.inbound.settings.testseed = [900, 500, 900, 256].slice();
}
}
if (dbInbound) {
this.dbInbound = new DBInbound(dbInbound);
} else {
@@ -42,6 +47,27 @@
loading(loading=true) {
inModal.confirmLoading = loading;
},
updateTestseed(index, value) {
if (!inModal.inbound || !inModal.inbound.settings) return;
if (!inModal.inbound.settings.testseed || !Array.isArray(inModal.inbound.settings.testseed)) {
inModal.inbound.settings.testseed = [900, 500, 900, 256];
}
while (inModal.inbound.settings.testseed.length <= index) {
inModal.inbound.settings.testseed.push(0);
}
inModal.inbound.settings.testseed[index] = value;
},
setRandomTestseed() {
if (!inModal.inbound || !inModal.inbound.settings) return;
if (!inModal.inbound.settings.testseed || !Array.isArray(inModal.inbound.settings.testseed) || inModal.inbound.settings.testseed.length < 4) {
inModal.inbound.settings.testseed = [900, 500, 900, 256].slice();
}
inModal.inbound.settings.testseed = [Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000)];
},
resetTestseed() {
if (!inModal.inbound || !inModal.inbound.settings) return;
inModal.inbound.settings.testseed = [900, 500, 900, 256].slice();
}
};
new Vue({
@@ -189,6 +215,22 @@
this.inbound.settings.decryption = 'none';
this.inbound.settings.encryption = 'none';
this.inbound.settings.selectedAuth = undefined;
},
updateTestseed(index, value) {
if (!this.inbound.settings.testseed || !Array.isArray(this.inbound.settings.testseed)) {
this.$set(this.inbound.settings, 'testseed', [900, 500, 900, 256]);
}
while (this.inbound.settings.testseed.length <= index) {
this.inbound.settings.testseed.push(0);
}
this.$set(this.inbound.settings.testseed, index, value);
},
setRandomTestseed() {
const newSeed = [Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000)];
this.$set(this.inbound.settings, 'testseed', newSeed);
},
resetTestseed() {
this.$set(this.inbound.settings, 'testseed', [900, 500, 900, 256]);
}
},
});

View File

@@ -299,7 +299,14 @@
v-model="fragmentLength" placeholder="100-200"></setting-list-item>
<setting-list-item style="padding: 10px 20px" type="text"
title='Interval' v-model="fragmentInterval"
placeholder="10-20"></setting-list-item>
placeholder="10-20">
</setting-list-item>
<a-setting-list-item paddings="small">
<template #title>MaxSplit</template>
<template #control>
<a-input type="text" v-model="fragmentMaxSplit" placeholder="300-400"></a-input>
</template>
</a-setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-list-item>
@@ -477,7 +484,8 @@
fragment: {
packets: "tlshello",
length: "100-200",
interval: "10-20"
interval: "10-20",
maxSplit: "300-400"
}
},
streamSettings: {
@@ -682,6 +690,16 @@
}
}
},
fragmentMaxSplit: {
get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.maxSplit : ""; },
set: function (v) {
if (v != "") {
newFragment = JSON.parse(this.allSetting.subJsonFragment);
newFragment.settings.fragment.maxSplit = v;
this.allSetting.subJsonFragment = JSON.stringify(newFragment);
}
}
},
noises: {
get() {
return this.allSetting?.subJsonNoises != "";

View File

@@ -856,7 +856,7 @@
tag: "direct",
protocol: "freedom"
},
routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
routingDomainStrategies: ["AsIs", "IpIfNonMatch", "IpOnDemand"],
log: {
loglevel: ["none", "debug", "info", "warning", "error"],
access: ["none", "./access.log"],
@@ -1087,7 +1087,9 @@
switch(o.protocol){
case Protocols.VMess:
case Protocols.VLESS:
serverObj = o.settings.vnext;
if (o.settings && o.settings.address && o.settings.port) {
return [o.settings.address + ':' + o.settings.port];
}
break;
case Protocols.HTTP:
case Protocols.Socks:

View File

@@ -20,7 +20,11 @@ func NewCheckCpuJob() *CheckCpuJob {
// Here run is a interface method of Job interface
func (j *CheckCpuJob) Run() {
threshold, _ := j.settingService.GetTgCpu()
threshold, err := j.settingService.GetTgCpu()
if err != nil || threshold <= 0 {
// If threshold cannot be retrieved or is not set, skip sending notifications
return
}
// get latest status of server
percent, err := cpu.Percent(1*time.Second, false)

View File

@@ -48,10 +48,10 @@ func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
return nil
}
func createTemplateData(params []string, separator ...string) map[string]interface{} {
func createTemplateData(params []string, seperator ...string) map[string]interface{} {
var sep string = "=="
if len(separator) > 0 {
sep = separator[0]
if len(seperator) > 0 {
sep = seperator[0]
}
templateData := make(map[string]interface{})

View File

@@ -1088,7 +1088,7 @@ func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model
clientTraffic.Email = client.Email
clientTraffic.Total = client.TotalGB
clientTraffic.ExpiryTime = client.ExpiryTime
clientTraffic.Enable = true
clientTraffic.Enable = client.Enable
clientTraffic.Up = 0
clientTraffic.Down = 0
clientTraffic.Reset = client.Reset
@@ -1101,7 +1101,7 @@ func (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *mod
result := tx.Model(xray.ClientTraffic{}).
Where("email = ?", email).
Updates(map[string]interface{}{
"enable": true,
"enable": client.Enable,
"email": client.Email,
"total": client.TotalGB,
"expiry_time": client.ExpiryTime,
@@ -1404,6 +1404,9 @@ func (s *InboundService) MigrationRequirements() {
defer func() {
if err == nil {
tx.Commit()
if dbErr := db.Exec(`VACUUM "main"`).Error; dbErr != nil {
logger.Warningf("VACUUM failed: %v", dbErr)
}
} else {
tx.Rollback()
}

View File

@@ -244,7 +244,7 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
}
var versions []string
for _, release := range releases {
if release.TagName >= "v1.8.0" {
if release.TagName >= "v26.1.23" {
versions = append(versions, release.TagName)
}
}
@@ -395,14 +395,39 @@ func (s *ServerService) GetLogs(count string, level string, syslog string) []str
var lines []string
if syslog == "true" {
cmdArgs := []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", count, "-p", level}
// Run the command
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
// Check if running on Windows - journalctl is not available
if runtime.GOOS == "windows" {
return []string{"Syslog is not supported on Windows. Please use application logs instead by unchecking the 'Syslog' option."}
}
// Validate and sanitize count parameter
countInt, err := strconv.Atoi(count)
if err != nil || countInt < 1 || countInt > 10000 {
return []string{"Invalid count parameter - must be a number between 1 and 10000"}
}
// Validate level parameter - only allow valid syslog levels
validLevels := map[string]bool{
"0": true, "emerg": true,
"1": true, "alert": true,
"2": true, "crit": true,
"3": true, "err": true,
"4": true, "warning": true,
"5": true, "notice": true,
"6": true, "info": true,
"7": true, "debug": true,
}
if !validLevels[level] {
return []string{"Invalid level parameter - must be a valid syslog level"}
}
// Use hardcoded command with validated parameters
cmd := exec.Command("journalctl", "-u", "x-ui", "--no-pager", "-n", strconv.Itoa(countInt), "-p", level)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
err = cmd.Run()
if err != nil {
return []string{"Failed to run journalctl command!"}
return []string{"Failed to run journalctl command! Make sure systemd is available and x-ui service is registered."}
}
lines = strings.Split(out.String(), "\n")
} else {
@@ -495,14 +520,26 @@ func (s *ServerService) ImportDB(file multipart.File) error {
return common.NewErrorf("Error saving db: %v", err)
}
// Check if we can init db or not
err = database.InitDB(tempPath)
if err != nil {
return common.NewErrorf("Error checking db: %v", err)
// Close temp file before opening via sqlite
if err = tempFile.Close(); err != nil {
return common.NewErrorf("Error closing temporary db file: %v", err)
}
tempFile = nil
// Validate integrity (no migrations / side effects)
if err = database.ValidateSQLiteDB(tempPath); err != nil {
return common.NewErrorf("Invalid or corrupt db file: %v", err)
}
// Stop Xray
s.StopXrayService()
// Stop Xray (ignore error but log)
if errStop := s.StopXrayService(); errStop != nil {
logger.Warningf("Failed to stop Xray before DB import: %v", errStop)
}
// Close existing DB to release file locks (especially on Windows)
if errClose := database.CloseDB(); errClose != nil {
logger.Warningf("Failed to close existing DB before replacement: %v", errClose)
}
// Backup the current database for fallback
fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath())

View File

@@ -35,6 +35,9 @@ func (s *XrayService) GetXrayErr() error {
}
err := p.GetErr()
if err == nil {
return nil
}
if runtime.GOOS == "windows" && err.Error() == "exit status 1" {
// exit status 1 on Windows means that Xray process was killed

View File

@@ -417,6 +417,12 @@
"psk" = "PreShared Key"
"domainStrategy" = "Domain Strategy"
[pages.xray.tun]
"nameDesc" = "The name of the TUN interface. Default is 'xrayN', where N is some number"
"mtuDesc" = "Maximum Transmission Unit. The maximum size of data packets. Default is 1500"
"userLevel" = "User Level"
"userLevelDesc" = "All connections made through this inbound will use this user level. Default is 0"
[pages.xray.dns]
"enable" = "Enable DNS"
"enableDesc" = "Enables built-in DNS server."

View File

@@ -415,6 +415,12 @@
"psk" = "کلید مشترک"
"domainStrategy" = "استراتژی حل دامنه"
[pages.xray.tun]
"nameDesc" = "نام رابط TUN. مقدار پیش‌فرض 'xrayN', N یک عدد است"
"mtuDesc" = "واحد انتقال حداکثر. بیشترین اندازه بسته‌های داده. مقدار پیش‌فرض 1500 است"
"userLevel" = "سطح کاربر"
"userLevelDesc" = "تمام اتصالات انجام‌شده از طریق این ورودی از این سطح کاربری استفاده خواهند کرد. مقدار پیش‌فرض 0 است"
[pages.xray.dns]
"enable" = "فعال کردن حل دامنه"
"enableDesc" = "سرور حل دامنه داخلی را فعال می‌کند"

View File

@@ -417,6 +417,12 @@
"psk" = "Общий ключ"
"domainStrategy" = "Стратегия домена"
[pages.xray.tun]
"nameDesc" = "Имя интерфейса TUN. Значение по умолчанию - 'xrayN', где N - номер интерфейса."
"mtuDesc" = "Максимальная единица передачи. Максимальный размер пакетов данных. Значение по умолчанию - 1500"
"userLevel" = "Уровень пользователя"
"userLevelDesc" = "Все соединения, установленные через этот входящий поток, будут использовать этот уровень пользователя. Значение по умолчанию - 0"
[pages.xray.dns]
"enable" = "Включить DNS"
"enableDesc" = "Включить встроенный DNS-сервер"

View File

@@ -417,6 +417,12 @@
"psk" = "Khóa chia sẻ"
"domainStrategy" = "Chiến lược tên miền"
[pages.xray.tun]
"nameDesc" = "Tên của giao diện TUN. Giá trị mặc định là 'xrayN', với N là số nguyen."
"mtuDesc" = "Đơn vị Truyền Tối đa. Kích thước tối đa của các gói dữ liệu. Giá trị mặc định là 1500"
"userLevel" = "Mức Người Dùng"
"userLevelDesc" = "Tất cả các kết nối được thực hiện thông qua inbound này sẽ sử dụng mức người dùng này. Giá trị mặc định là 0"
[pages.xray.dns]
"enable" = "Kích hoạt DNS"
"enableDesc" = "Kích hoạt máy chủ DNS tích hợp"

View File

@@ -417,6 +417,12 @@
"psk" = "共享密钥"
"domainStrategy" = "域策略"
[pages.xray.tun]
"nameDesc" = "TUN 接口的名称。默认值为 'xrayN', 其中 N 是接口的编号。"
"mtuDesc" = "最大传输单元。数据包的最大大小。默认值为 1500"
"userLevel" = "用户级别"
"userLevelDesc" = "通过此入站的所有连接都将使用此用户级别。默认值为 0"
[pages.xray.dns]
"enable" = "启用 DNS"
"enableDesc" = "启用内置 DNS 服务器"

View File

@@ -230,6 +230,10 @@ func (s *Server) initRouter() (*gin.Engine, error) {
s.xui = controller.NewXUIController(g)
s.api = controller.NewAPIController(g, s.server)
engine.NoRoute(func(c *gin.Context) {
c.AbortWithStatus(http.StatusNotFound)
})
return engine, nil
}