Browse Source

improve reality setting

split xtls from tls - remove iran warp - remove old setting reality from franzkafka (it was a messy code) -and other improvement

Co-Authored-By: Alireza Ahmadi <[email protected]>
MHSanaei 1 year ago
parent
commit
3e0faecaae

+ 5 - 0
web/assets/css/custom.css

@@ -246,6 +246,11 @@
     background-color: #2e3b52;
 }
 
+.ant-card-dark .ant-select-disabled .ant-select-selection {
+    border: 1px solid rgba(255, 255, 255, 0.2);
+    background-color: #242c3a;
+}
+
 .ant-card-dark .ant-collapse-item {
     color: hsla(0,0%,100%,.65);
     background-color: #161b22;

+ 227 - 106
web/assets/js/model/xray.js

@@ -49,6 +49,7 @@ const XTLS_FLOW_CONTROL = {
 
 const TLS_FLOW_CONTROL = {
     VISION: "xtls-rprx-vision",
+    VISION_UDP443: "xtls-rprx-vision-udp443",
 };
 
 const TLS_VERSION_OPTION = {
@@ -91,9 +92,6 @@ const UTLS_FINGERPRINT = {
     UTLS_RANDOMIZED: "randomized",
 };
 
-const bytesToHex = e => Array.from(e).map(e => e.toString(16).padStart(2, 0)).join('');
-const hexToBytes = e => new Uint8Array(e.match(/[0-9a-f]{2}/gi).map(e => parseInt(e, 16)));
-
 const ALPN_OPTION = {
     H3: "h3",
     H2: "h2",
@@ -481,7 +479,7 @@ class TlsStreamSettings extends XrayCommonClass {
                 cipherSuites = '',
                 certificates=[new TlsStreamSettings.Cert()],
                 alpn=[ALPN_OPTION.H2,ALPN_OPTION.HTTP1],
-                settings=[new TlsStreamSettings.Settings()]) {
+                settings=new TlsStreamSettings.Settings()) {
         super();
         this.server = serverName;
         this.minVersion = minVersion;
@@ -508,8 +506,7 @@ class TlsStreamSettings extends XrayCommonClass {
         }
 
 		if (!ObjectUtil.isEmpty(json.settings)) {
-            let values = json.settings[0];
-            settings = [new TlsStreamSettings.Settings(values.allowInsecure , values.fingerprint, values.serverName)];
+            settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName);
         }
         return new TlsStreamSettings(
             json.serverName,
@@ -530,7 +527,7 @@ class TlsStreamSettings extends XrayCommonClass {
             cipherSuites: this.cipherSuites,
             certificates: TlsStreamSettings.toJsonArray(this.certs),
             alpn: this.alpn,
-            settings: TlsStreamSettings.toJsonArray(this.settings),
+            settings: this.settings,
         };
     }
 }
@@ -598,71 +595,204 @@ TlsStreamSettings.Settings = class extends XrayCommonClass {
         };
     }
 };
+class XtlsStreamSettings extends XrayCommonClass {
+    constructor(serverName='',
+                certificates=[new XtlsStreamSettings.Cert()],
+                alpn=[ALPN_OPTION.H2,ALPN_OPTION.HTTP1],
+                settings=new XtlsStreamSettings.Settings()) {
+        super();
+        this.server = serverName;
+        this.certs = certificates;
+        this.alpn = alpn;
+        this.settings = settings;
+    }
+
+    addCert(cert) {
+        this.certs.push(cert);
+    }
+
+    removeCert(index) {
+        this.certs.splice(index, 1);
+    }
+
+    static fromJson(json={}) {
+        let certs;
+        let settings;
+        if (!ObjectUtil.isEmpty(json.certificates)) {
+            certs = json.certificates.map(cert => XtlsStreamSettings.Cert.fromJson(cert));
+        }
+
+		if (!ObjectUtil.isEmpty(json.settings)) {
+            settings = new XtlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.serverName);
+        }
+        return new XtlsStreamSettings(
+            json.serverName,
+            certs,
+            json.alpn,
+            settings,
+        );
+    }
+
+    toJson() {
+        return {
+            serverName: this.server,
+            certificates: XtlsStreamSettings.toJsonArray(this.certs),
+            alpn: this.alpn,
+            settings: this.settings,
+        };
+    }
+}
+
+XtlsStreamSettings.Cert = class extends XrayCommonClass {
+    constructor(useFile=true, certificateFile='', keyFile='', certificate='', key='') {
+        super();
+        this.useFile = useFile;
+        this.certFile = certificateFile;
+        this.keyFile = keyFile;
+        this.cert = certificate instanceof Array ? certificate.join('\n') : certificate;
+        this.key = key instanceof Array ? key.join('\n') : key;
+    }
+
+    static fromJson(json={}) {
+        if ('certificateFile' in json && 'keyFile' in json) {
+            return new XtlsStreamSettings.Cert(
+                true,
+                json.certificateFile,
+                json.keyFile,
+            );
+        } else {
+            return new XtlsStreamSettings.Cert(
+                false, '', '',
+                json.certificate.join('\n'),
+                json.key.join('\n'),
+            );
+        }
+    }
+
+    toJson() {
+        if (this.useFile) {
+            return {
+                certificateFile: this.certFile,
+                keyFile: this.keyFile,
+            };
+        } else {
+            return {
+                certificate: this.cert.split('\n'),
+                key: this.key.split('\n'),
+            };
+        }
+    }
+};
+
+XtlsStreamSettings.Settings = class extends XrayCommonClass {
+    constructor(allowInsecure = false, serverName = '') {
+        super();
+        this.allowInsecure = allowInsecure;
+        this.serverName = serverName;
+    }
+    static fromJson(json = {}) {
+        return new XtlsStreamSettings.Settings(
+            json.allowInsecure,
+            json.servername,
+        );
+    }
+    toJson() {
+        return {
+            allowInsecure: this.allowInsecure,
+            serverName: this.serverName,
+        };
+    }
+};
 
 class RealityStreamSettings extends XrayCommonClass {
     
     constructor(
         show = false,xver = 0,
-        fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX,
         dest = 'yahoo.com:443',
         serverNames = 'yahoo.com,www.yahoo.com',
-        privateKey = RandomUtil.randomX25519PrivateKey(),
-        publicKey = '',
+        privateKey = '',
         minClient = '',
         maxClient = '',
         maxTimediff = 0,
-        shortIds = RandomUtil.randowShortId() 
-        )   
-        {
+        shortIds = RandomUtil.randowShortId(),
+        settings= new RealityStreamSettings.Settings()
+        ){
         super();
         this.show = show;
         this.xver = xver;
-        this.fingerprint = fingerprint;
         this.dest = dest;
         this.serverNames = serverNames instanceof Array ? serverNames.join(",") : serverNames;
         this.privateKey = privateKey;
-        this.publicKey = RandomUtil.randomX25519PublicKey(this.privateKey);
         this.minClient = minClient;
         this.maxClient = maxClient;
         this.maxTimediff = maxTimediff;
         this.shortIds = shortIds instanceof Array ? shortIds.join(",") : shortIds; 
+        this.settings = settings;
+    }
+
+    static fromJson(json = {}) {
+        let settings;
+		if (!ObjectUtil.isEmpty(json.settings)) {
+            settings = new RealityStreamSettings.Settings(json.settings.publicKey , json.settings.fingerprint, json.settings.serverName);
         }
-        static fromJson(json = {}) {
         return new RealityStreamSettings(
             json.show,
             json.xver,
-            json.fingerprint,
             json.dest,
             json.serverNames,
             json.privateKey,
-            json.publicKey,
             json.minClient,
             json.maxClient,
             json.maxTimediff,
-            json.shortIds  
+            json.shortIds,
+            json.settings,
         );
-        }
-        toJson() {
+
+    }
+    toJson() {
         return {
             show: this.show,
             xver: this.xver,
-            fingerprint: this.fingerprint,
             dest: this.dest,
-            serverNames: this.serverNames.split(/,|,|\s+/),
+            serverNames: this.serverNames.split(","),
             privateKey: this.privateKey,
-            publicKey: this.publicKey,
             minClient: this.minClient,
             maxClient: this.maxClient,
             maxTimediff: this.maxTimediff,
-            shortIds: this.shortIds.split(/,|,|\s+/)
-            };
-        }
+            shortIds: this.shortIds.split(","),
+            settings: this.settings,
+        };
     }
+}
+
+RealityStreamSettings.Settings = class extends XrayCommonClass {
+    constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, serverName = '') {
+        super();
+        this.publicKey = publicKey;
+        this.fingerprint = fingerprint;
+        this.serverName = serverName;
+    }
+    static fromJson(json = {}) {
+        return new RealityStreamSettings.Settings(
+            json.publicKey,
+            json.fingerprint,
+            json.serverName,
+        );
+    }
+    toJson() {
+        return {
+            publicKey: this.publicKey,
+            fingerprint: this.fingerprint,
+            serverName: this.serverName,
+        };
+    }
+};
 
 class StreamSettings extends XrayCommonClass {
     constructor(network='tcp',
         security='none',
         tlsSettings=new TlsStreamSettings(),
+        xtlsSettings=new XtlsStreamSettings(),
         realitySettings = new RealityStreamSettings(),
         tcpSettings=new TcpStreamSettings(),
         kcpSettings=new KcpStreamSettings(),
@@ -675,6 +805,7 @@ class StreamSettings extends XrayCommonClass {
         this.network = network;
         this.security = security;
         this.tls = tlsSettings;
+        this.xtls = xtlsSettings;
         this.reality = realitySettings;
         this.tcp = tcpSettings;
         this.kcp = kcpSettings;
@@ -685,7 +816,7 @@ class StreamSettings extends XrayCommonClass {
     }
 
     get isTls() {
-        return this.security === 'tls';
+        return this.security === "tls";
     }
 
     set isTls(isTls) {
@@ -696,12 +827,12 @@ class StreamSettings extends XrayCommonClass {
         }
     }
 
-    get isXTLS() {
+    get isXtls() {
         return this.security === "xtls";
     }
 
-    set isXTLS(isXTLS) {
-        if (isXTLS) {
+    set isXtls(isXtls) {
+        if (isXtls) {
             this.security = 'xtls';
         } else {
             this.security = 'none';
@@ -715,27 +846,19 @@ class StreamSettings extends XrayCommonClass {
 
     set isReality(isReality) {
         if (isReality) {
-            this.security = "reality";
+            this.security = 'reality';
         } else {
-            this.security = "none";
+            this.security = 'none';
         }
     }
-    
-    static fromJson(json = {}) {
-        let tls, reality;
-        if (json.security === "xtls") {
-            tls = TlsStreamSettings.fromJson(json.XTLSSettings);
-        } else if (json.security === "tls") {
-            tls = TlsStreamSettings.fromJson(json.tlsSettings);
-        }
-        if (json.security === "reality") {
-            reality = RealityStreamSettings.fromJson(json.realitySettings)
-        }
+
+    static fromJson(json={}) {
         return new StreamSettings(
             json.network,
             json.security,
-            tls,
-            reality,
+            TlsStreamSettings.fromJson(json.tlsSettings),
+            XtlsStreamSettings.fromJson(json.xtlsSettings),
+            RealityStreamSettings.fromJson(json.realitySettings),
             TcpStreamSettings.fromJson(json.tcpSettings),
             KcpStreamSettings.fromJson(json.kcpSettings),
             WsStreamSettings.fromJson(json.wsSettings),
@@ -751,9 +874,9 @@ class StreamSettings extends XrayCommonClass {
             network: network,
             security: this.security,
             tlsSettings: this.isTls ? this.tls.toJson() : undefined,
-            XTLSSettings: this.isXTLS ? this.tls.toJson() : undefined,
-            tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,
+            xtlsSettings: this.isXtls ? this.xtls.toJson() : undefined,
             realitySettings: this.isReality ? this.reality.toJson() : undefined,
+            tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,
             kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined,
             wsSettings: network === 'ws' ? this.ws.toJson() : undefined,
             httpSettings: network === 'http' ? this.http.toJson() : undefined,
@@ -826,22 +949,18 @@ class Inbound extends XrayCommonClass {
 
     set tls(isTls) {
         if (isTls) {
-            this.xtls = false;
-            this.reality = false;
             this.stream.security = 'tls';
         } else {
             this.stream.security = 'none';
         }
     }
 
-    get XTLS() {
+    get xtls() {
         return this.stream.security === 'xtls';
     }
 
-    set XTLS(isXTLS) {
-        if (isXTLS) {
-            this.xtls = false;
-            this.reality = false;
+    set xtls(isXtls) {
+        if (isXtls) {
             this.stream.security = 'xtls';
         } else {
             this.stream.security = 'none';
@@ -850,19 +969,14 @@ class Inbound extends XrayCommonClass {
 
     //for Reality
     get reality() {
-        if (this.stream.security === "reality") {
-            return this.network === "tcp" || this.network === "grpc" || this.network === "http";
-        }
-        return false;
+        return this.stream.security === 'reality';
     }
 
     set reality(isReality) {
         if (isReality) {
-            this.tls = false;
-            this.xtls = false;
-            this.stream.security = "reality";
+            this.stream.security = 'reality';
         } else {
-            this.stream.security = "none";
+            this.stream.security = 'none';
         }
     }
 
@@ -969,7 +1083,7 @@ class Inbound extends XrayCommonClass {
     }
 
     get serverName() {
-        if (this.stream.isTls || this.stream.isXTLS) {
+        if (this.stream.isTls || this.stream.isXtls || this.stream.isReality) {
             return this.stream.tls.server;
         }
         return "";
@@ -1070,7 +1184,14 @@ class Inbound extends XrayCommonClass {
             default:
                 return false;
         }
-        return this.network === "tcp" || this.network === "grpc" || this.network === "http";
+        switch (this.network) {
+            case "tcp":
+            case "http":
+            case "grpc":
+                return true;
+            default:
+                return false;
+        }
     }
 
     //this is used for xtls-rprx-vision
@@ -1090,7 +1211,7 @@ class Inbound extends XrayCommonClass {
         return this.canEnableTls();
     }
 
-    canEnableXTLS() {
+    canEnableXtls() {
         switch (this.protocol) {
             case Protocols.VLESS:
             case Protocols.TROJAN:
@@ -1195,10 +1316,10 @@ class Inbound extends XrayCommonClass {
             host: host,
             path: path,
             tls: this.stream.security,
-            sni: this.stream.tls.settings[0]['serverName'],
-            fp: this.stream.tls.settings[0]['fingerprint'],
+            sni: this.stream.tls.settings.serverName,
+            fp: this.stream.tls.settings.fingerprint,
             alpn: this.stream.tls.alpn.join(','),
-            allowInsecure: this.stream.tls.settings[0].allowInsecure,
+            allowInsecure: this.stream.tls.settings.allowInsecure,
         };
         return 'vmess://' + base64(JSON.stringify(obj, null, 2));
     }
@@ -1257,54 +1378,54 @@ class Inbound extends XrayCommonClass {
 
         if (this.tls) {
             params.set("security", "tls");
-            params.set("fp" , this.stream.tls.settings[0]['fingerprint']);
+            params.set("fp" , this.stream.tls.settings.fingerprint);
             params.set("alpn", this.stream.tls.alpn);
-            if(this.stream.tls.settings[0].allowInsecure){
+            if(this.stream.tls.settings.allowInsecure){
                 params.set("allowInsecure", "1");
             }
             if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
                 address = this.stream.tls.server;
 			}
-            if (this.stream.tls.settings[0]['serverName'] !== ''){
-                params.set("sni", this.stream.tls.settings[0]['serverName']);
+            if (this.stream.tls.settings.serverName !== ''){
+                params.set("sni", this.stream.tls.settings.serverName);
             }
             if (type === "tcp" && this.settings.vlesses[clientIndex].flow.length > 0) {
                 params.set("flow", this.settings.vlesses[clientIndex].flow);
             }
         }
 
-        if (this.XTLS) {
+        if (this.xtls) {
             params.set("security", "xtls");
-            params.set("alpn", this.stream.tls.alpn);
-            if(this.stream.tls.settings[0].allowInsecure){
+            params.set("alpn", this.stream.xtls.alpn);
+            if(this.stream.xtls.settings.allowInsecure){
                 params.set("allowInsecure", "1");
             }
-            if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
-                address = this.stream.tls.server;
-            }
+            if (!ObjectUtil.isEmpty(this.stream.xtls.server)) {
+                address = this.stream.xtls.server;
+			}
             params.set("flow", this.settings.vlesses[clientIndex].flow);
         }
 
         if (this.reality) {
             params.set("security", "reality");
+            params.set("pbk", this.stream.reality.settings.publicKey);
             if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
-                params.set("sni", this.stream.reality.serverNames.split(/,|,|\s+/)[0]);
-            }
-            if (this.stream.reality.publicKey != "") {
-                //params.set("pbk", Ed25519.getPublicKey(this.stream.reality.privateKey));
-                params.set("pbk", this.stream.reality.publicKey);
+                params.set("sni", this.stream.reality.serverNames.split(",")[0]);
             }
             if (this.stream.network === 'tcp') {
                 params.set("flow", this.settings.vlesses[clientIndex].flow);
             }
             if (this.stream.reality.shortIds != "") {
-                params.set("sid", this.stream.reality.shortIds);
+                params.set("sid", this.stream.reality.shortIds.split(",")[0]);
+            }
+            if (this.stream.reality.settings.fingerprint != "") {
+                params.set("fp", this.stream.reality.settings.fingerprint);
             }
-            if (this.stream.reality.fingerprint != "") {
-                params.set("fp", this.stream.reality.fingerprint);
+            if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
+                address = this.stream.reality.settings.serverName;
             }
         }
-        
+
         const link = `vless://${uuid}@${address}:${port}`;
         const url = new URL(link);
         for (const [key, value] of params) {
@@ -1376,47 +1497,47 @@ class Inbound extends XrayCommonClass {
 
         if (this.tls) {
             params.set("security", "tls");
-            params.set("fp" , this.stream.tls.settings[0]['fingerprint']);
+            params.set("fp" , this.stream.tls.settings.fingerprint);
             params.set("alpn", this.stream.tls.alpn);
-            if(this.stream.tls.settings[0].allowInsecure){
+            if(this.stream.tls.settings.allowInsecure){
                 params.set("allowInsecure", "1");
             }
             if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
                 address = this.stream.tls.server;
             }
-            if (this.stream.tls.settings[0]['serverName'] !== ''){
-                params.set("sni", this.stream.tls.settings[0]['serverName']);
+            if (this.stream.tls.settings.serverName !== ''){
+                params.set("sni", this.stream.tls.settings.serverName);
 			}
         }
 
         if (this.reality) {
             params.set("security", "reality");
+            params.set("pbk", this.stream.reality.settings.publicKey);
             if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
-                params.set("sni", this.stream.reality.serverNames.split(/,|,|\s+/)[0]);
+                params.set("sni", this.stream.reality.serverNames.split(",")[0]);
             }
-            if (this.stream.reality.publicKey != "") {
-                //params.set("pbk", Ed25519.getPublicKey(this.stream.reality.privateKey));
-                params.set("pbk", this.stream.reality.publicKey);
-            }
-            if (this.stream.network === 'tcp') {
-                params.set("flow", this.settings.trojans[clientIndex].flow);
+            if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
+                address = this.stream.reality.settings.serverName;
             }
             if (this.stream.reality.shortIds != "") {
-                params.set("sid", this.stream.reality.shortIds);
+                params.set("sid", this.stream.reality.shortIds.split(",")[0]);
+            }
+            if (this.stream.reality.settings.fingerprint != "") {
+                params.set("fp", this.stream.reality.settings.fingerprint);
             }
-            if (this.stream.reality.fingerprint != "") {
-                params.set("fp", this.stream.reality.fingerprint);
+            if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
+                address = this.stream.reality.settings.serverName;
             }
         }
 
-		if (this.XTLS) {
+		if (this.xtls) {
             params.set("security", "xtls");
-            params.set("alpn", this.stream.tls.alpn);
-            if(this.stream.tls.settings[0].allowInsecure){
+            params.set("alpn", this.stream.xtls.alpn);
+            if(this.stream.xtls.settings.allowInsecure){
                 params.set("allowInsecure", "1");
             }
             if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
-                address = this.stream.tls.server;
+                address = this.stream.xtls.server;
 			}
             params.set("flow", this.settings.trojans[clientIndex].flow);
         }

+ 6 - 40
web/assets/js/util/utils.js

@@ -94,26 +94,6 @@ const shortIdSeq = [
     '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
 ];
 
-const x25519Map = new Map(
-    [
-        ['EH2FWe-Ij_FFAa2u9__-aiErLvVIneP601GOCdlyPWw', "goY3OtfaA4UYbiz7Hn0NysI5QJrK0VT_Chg6RLgUPQU"],
-        ['cKI_6DoMSP1IepeWWXrG3G9nkehl94KYBhagU50g2U0', "VigpKFbSLnHLzBWobZaS1IBmw--giJ51w92y723ajnU"],
-        ['qM2SNyK3NyHB6deWpEP3ITyCGKQFRTna_mlKP0w1QH0', "HYyIGuyNFslmcnNT7mrDdmuXwn4cm7smE_FZbYguKHQ"],
-        ['qCWg5GMEDFd3n1nxDswlIpOHoPUXMLuMOIiLUVzubkI', "rJFC3dUjJxMnVZiUGzmf_LFsJUwFWY-CU5RQgFOHCWM"],
-        ['4NOBxDrEsOhNI3Y3EnVIy_TN-uyBoAjQw6QM0YsOi0s', "CbcY9qc4YuMDJDyyL0OITlU824TBg1O84ClPy27e2RM"],
-        ['eBvFb0M4HpSOwWjtXV8zliiEs_hg56zX4a2LpuuqpEI', "CjulQ2qVIky7ImIfysgQhNX7s_drGLheCGSkVHcLZhc"],
-        ['yEpOzQV04NNcycWVeWtRNTzv5TS-ynTuKRacZCH-6U8', "O9RSr5gSdok2K_tobQnf_scyKVqnCx6C4Jrl7_rCZEQ"],
-        ['CNt6TAUVCwqM6xIBHyni0K3Zqbn2htKQLvLb6XDgh0s', "d9cGLVBrDFS02L2OvkqyqwFZ1Ux3AHs28ehl4Rwiyl0"],
-        ['EInKw-6Wr0rAHXlxxDuZU5mByIzcD3Z-_iWPzXlUL1k', "LlYD2nNVAvyjNvjZGZh4R8PkMIwkc6EycPTvR2LE0nQ"],
-        ['GKIKo7rcXVyle-EUHtGIDtYnDsI6osQmOUl3DTJRAGc', "VcqHivYGGoBkcxOI6cSSjQmneltstkb2OhvO53dyhEM"],
-        ['-FVDzv68IC17fJVlNDlhrrgX44WeBfbhwjWpCQVXGHE', "PGG2EYOvsFt2lAQTD7lqHeRxz2KxvllEDKcUrtizPBU"],
-        ['0H3OJEYEu6XW7woqy7cKh2vzg6YHkbF_xSDTHKyrsn4', "mzevpYbS8kXengBY5p7tt56QE4tS3lwlwRemmkcQeyc"],
-        ['8F8XywN6ci44ES6em2Z0fYYxyptB9uaXY9Hc1WSSPE4', "qCZUdWQZ2H33vWXnOkG8NpxBeq3qn5QWXlfCOWBNkkc"],
-        ['IN0dqfkC10dj-ifRHrg2PmmOrzYs697ajGMwcLbu-1g', "2UW_EO3r7uczPGUUlpJBnMDpDmWUHE2yDzCmXS4sckE"],
-        ['uIcmks5rAhvBe4dRaJOdeSqgxLGGMZhsGk4J4PEKL2s', "F9WJV_74IZp0Ide4hWjiJXk9FRtBUBkUr3mzU-q1lzk"],
-    ]
-);
-
 class RandomUtil {
 
     static randomIntRange(min, max) {
@@ -170,26 +150,6 @@ class RandomUtil {
         });
     }
 
-    static randowShortId() {
-        let str = '';
-        str += this.randomShortIdSeq(8)
-        return str;
-    }
-
-    static randomX25519PrivateKey() {
-        let num = x25519Map.size;
-        let index = this.randomInt(num);
-        let cntr = 0;
-        for (let key of x25519Map.keys()) {
-            if (cntr++ === index) {
-                return key;
-            }
-        }
-    }
-
-    static randomX25519PublicKey(key) {
-        return x25519Map.get(key)
-    }
     static randomText() {
         var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
         var string = '';
@@ -199,6 +159,12 @@ class RandomUtil {
         }
         return string;
     }
+
+    static randowShortId() {
+        let str = '';
+        str += this.randomShortIdSeq(8)
+        return str;
+    }
 }
 
 class ObjectUtil {

+ 5 - 5
web/controller/inbound.go

@@ -33,7 +33,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
 	g.POST("/update/:id", a.updateInbound)
 	g.POST("/clientIps/:email", a.getClientIps)
 	g.POST("/clearClientIps/:email", a.clearClientIps)
-	g.POST("/addClient/", a.addInboundClient)
+	g.POST("/addClient", a.addInboundClient)
 	g.POST("/delClient/:email", a.delInboundClient)
 	g.POST("/updateClient/:index", a.updateInboundClient)
 	g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
@@ -151,19 +151,19 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
 	jsonMsg(c, "Log Cleared", nil)
 }
 func (a *InboundController) addInboundClient(c *gin.Context) {
-	inbound := &model.Inbound{}
-	err := c.ShouldBind(inbound)
+	data := &model.Inbound{}
+	err := c.ShouldBind(data)
 	if err != nil {
 		jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
 		return
 	}
 
-	err = a.inboundService.AddInboundClient(inbound)
+	err = a.inboundService.AddInboundClient(data)
 	if err != nil {
 		jsonMsg(c, "something worng!", err)
 		return
 	}
-	jsonMsg(c, "Client added", nil)
+	jsonMsg(c, "Client(s) added", nil)
 	if err == nil {
 		a.xrayService.SetToNeedRestart()
 	}

+ 13 - 3
web/controller/server.go

@@ -41,6 +41,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
 	g.POST("/logs/:count", a.getLogs)
 	g.POST("/getConfigJson", a.getConfigJson)
 	g.GET("/getDb", a.getDb)
+	g.POST("/getNewX25519Cert", a.getNewX25519Cert)
 }
 
 func (a *ServerController) refreshStatus() {
@@ -114,7 +115,7 @@ func (a *ServerController) getLogs(c *gin.Context) {
 	count := c.Param("count")
 	logs, err := a.serverService.GetLogs(count)
 	if err != nil {
-		jsonMsg(c, I18n(c, "getLogs"), err)
+		jsonMsg(c, "getLogs", err)
 		return
 	}
 	jsonObj(c, logs, nil)
@@ -123,7 +124,7 @@ func (a *ServerController) getLogs(c *gin.Context) {
 func (a *ServerController) getConfigJson(c *gin.Context) {
 	configJson, err := a.serverService.GetConfigJson()
 	if err != nil {
-		jsonMsg(c, I18n(c, "getLogs"), err)
+		jsonMsg(c, "get config.json", err)
 		return
 	}
 	jsonObj(c, configJson, nil)
@@ -132,7 +133,7 @@ func (a *ServerController) getConfigJson(c *gin.Context) {
 func (a *ServerController) getDb(c *gin.Context) {
 	db, err := a.serverService.GetDb()
 	if err != nil {
-		jsonMsg(c, I18n(c, "getLogs"), err)
+		jsonMsg(c, "get Database", err)
 		return
 	}
 	// Set the headers for the response
@@ -142,3 +143,12 @@ func (a *ServerController) getDb(c *gin.Context) {
 	// Write the file contents to the response
 	c.Writer.Write(db)
 }
+
+func (a *ServerController) getNewX25519Cert(c *gin.Context) {
+	cert, err := a.serverService.GetNewX25519Cert()
+	if err != nil {
+		jsonMsg(c, "get x25519 certificate", err)
+		return
+	}
+	jsonObj(c, cert, nil)
+}

+ 6 - 2
web/controller/sub.go

@@ -29,14 +29,18 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
 func (a *SUBController) subs(c *gin.Context) {
 	subId := c.Param("subid")
 	host := strings.Split(c.Request.Host, ":")[0]
-	subs, err := a.subService.GetSubs(subId, host)
-	if err != nil {
+	subs, header, err := a.subService.GetSubs(subId, host)
+	if err != nil || len(subs) == 0 {
 		c.String(400, "Error!")
 	} else {
 		result := ""
 		for _, sub := range subs {
 			result += sub + "\n"
 		}
+
+		// Add subscription-userinfo
+		c.Writer.Header().Set("subscription-userinfo", header)
+
 		c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
 	}
 }

+ 40 - 6
web/html/xui/client_bulk_modal.html

@@ -33,6 +33,30 @@
             <span slot="label">{{ i18n "pages.client.clientCount" }}</span>
             <a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
         </a-form-item>
+        <a-form-item>
+            <span slot="label">
+                <span>{{ i18n "pages.inbounds.IPLimit" }}</span>
+                <a-tooltip>
+                    <template slot="title">
+                    <span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
+                    </template>
+                    <a-icon type="question-circle" theme="filled"></a-icon>
+                </a-tooltip>
+            </span>
+            <a-input type="number" v-model.number="clientsBulkModal.limitIp" min="0" style="width: 70px;" ></a-input>
+        </a-form-item>
+        <a-form-item v-if="clientsBulkModal.inbound.xtls" label="Flow">
+            <a-select v-model="clientsBulkModal.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
+                <a-select-option value="">{{ i18n "none" }}</a-select-option>
+                <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
+            </a-select>
+        </a-form-item>
+        <a-form-item v-if="clientsBulkModal.inbound.canEnableTlsFlow()" label="Flow" layout="inline">
+            <a-select v-model="clientsBulkModal.flow" style="width: 150px">
+                <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
+                <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
+            </a-select>
+        </a-form-item>
         <a-form-item label="Subscription">
             <a-input v-model.trim="clientsBulkModal.subId"></a-input>
         </a-form-item>
@@ -51,10 +75,10 @@
             </span>
         <a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
         </a-form-item>
-        <a-form-item label="{{ i18n "pages.client.delayedStart" }}">
+        <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
             <a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
         </a-form-item>
-        <a-form-item label="{{ i18n "pages.client.expireDays" }}" v-if="clientsBulkModal.delayedStart">
+        <a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientsBulkModal.delayedStart">
             <a-input type="number" v-model.number="delayedExpireDays" :min="0"></a-input>
         </a-form-item>
         <a-form-item v-else>
@@ -83,9 +107,9 @@
         confirm: null,
         dbInbound: new DBInbound(),
         inbound: new Inbound(),
-        clients: [],
         quantity: 1,
         totalGB: 0,
+        limitIp: 0,
         expiryTime: '',
         emailMethod: 0,
         firstNum: 1,
@@ -94,8 +118,10 @@
         emailPostfix: "",
         subId: "",
         tgId: "",
+        flow: "",
         delayedStart: false,
         ok() {
+            clients = [];
             method=clientsBulkModal.emailMethod;
             if(method>1){
                 start=clientsBulkModal.firstNum;
@@ -113,11 +139,18 @@
                 newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
                 newClient.subId = clientsBulkModal.subId;
                 newClient.tgId = clientsBulkModal.tgId;
+                newClient.limitIp = clientsBulkModal.limitIp;
                 newClient._totalGB = clientsBulkModal.totalGB;
                 newClient._expiryTime = clientsBulkModal.expiryTime;
-                clientsBulkModal.clients.push(newClient);
+                if(clientsBulkModal.inbound.canEnableTlsFlow()){
+                    newClient.flow = clientsBulkModal.flow;
+                }
+                if(clientsBulkModal.inbound.xtls){
+                    newClient.flow = clientsBulkModal.flow;
+                }
+                clients.push(newClient);
             }
-            ObjectUtil.execute(clientsBulkModal.confirm, clientsBulkModal.inbound, clientsBulkModal.dbInbound);
+            ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id);
         },
         show({ title='', okText='{{ i18n "sure" }}', dbInbound=null, confirm=(inbound, dbInbound)=>{} }) {
             this.visible = true;
@@ -128,15 +161,16 @@
             this.totalGB = 0;
             this.expiryTime = 0;
             this.emailMethod= 0;
+            this.limitIp= 0;
             this.firstNum= 1;
             this.lastNum= 1;
             this.emailPrefix= "";
             this.emailPostfix= "";
             this.subId= "";
             this.tgId= "";
+            this.flow= "";
             this.dbInbound = new DBInbound(dbInbound);
             this.inbound = dbInbound.toInbound();
-            this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
             this.delayedStart = false;
         },
         getClients(protocol, clientSettings) {

+ 7 - 2
web/html/xui/client_modal.html

@@ -12,6 +12,7 @@
         confirmLoading: false,
         title: '',
         okText: '',
+        isEdit: false,
         dbInbound: new DBInbound(),
         inbound: new Inbound(),
         clients: [],
@@ -21,9 +22,13 @@
         isExpired: false,
         delayedStart: false,
         ok() {
-            ObjectUtil.execute(clientModal.confirm, clientModal.inbound, clientModal.dbInbound, clientModal.index);
+            if(clientModal.isEdit){
+                ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.index);
+            } else {
+                ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
+            }
         },
-        show({ title='', okText='{{ i18n "sure" }}', index=null, dbInbound=null, confirm=(index, dbInbound)=>{}, isEdit=false  }) {
+        show({ title='', okText='{{ i18n "sure" }}', index=null, dbInbound=null, confirm=()=>{}, isEdit=false  }) {
             this.visible = true;
             this.title = title;
             this.okText = okText;

+ 3 - 3
web/html/xui/form/client.html

@@ -68,7 +68,7 @@
 			</a-textarea>
 		</a-form>
 	</a-form-item>
-    <a-form-item v-if="inbound.XTLS" label="Flow">
+    <a-form-item v-if="inbound.xtls" label="Flow">
         <a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
             <a-select-option value="">{{ i18n "none" }}</a-select-option>
             <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
@@ -100,10 +100,10 @@
             </a-tag>
         </template>
     </a-form-item>
-    <a-form-item label="{{ i18n "pages.client.delayedStart" }}">
+    <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
         <a-switch v-model="clientModal.delayedStart" @click="client._expiryTime=0"></a-switch>
     </a-form-item>
-    <a-form-item label="{{ i18n "pages.client.expireDays" }}" v-if="clientModal.delayedStart">
+    <a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientModal.delayedStart">
         <a-input type="number" v-model.number="delayedExpireDays" :min="0"></a-input>
     </a-form-item>
     <a-form-item v-else>

+ 2 - 2
web/html/xui/form/protocol/trojan.html

@@ -1,7 +1,7 @@
 {{define "form/trojan"}}
 <a-form layout="inline">
 <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">  
-    <a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
+    <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
         <a-form layout="inline">
             <a-form-item>
                 <span slot="label">
@@ -31,7 +31,7 @@
                 </span>
                 <a-input type="number" v-model.number="client.limitIp" min="0"  style="width: 70px;" ></a-input>
 		</a-form-item>
-        <a-form-item v-if="inbound.XTLS" label="Flow">
+        <a-form-item v-if="inbound.xtls" label="Flow">
             <a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
                 <a-select-option value="">{{ i18n "none" }}</a-select-option>
                 <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>

+ 2 - 2
web/html/xui/form/protocol/vless.html

@@ -1,7 +1,7 @@
 {{define "form/vless"}}
 <a-form layout="inline">
 <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">    
-    <a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
+    <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
         <a-form layout="inline">
             <a-form-item>
                 <span slot="label">
@@ -31,7 +31,7 @@
             </span>
             <a-input type="number" v-model.number="client.limitIp" min="0"  style="width: 70px;" ></a-input>
 		</a-form-item>
-		<a-form-item v-if="inbound.XTLS" label="Flow">
+		<a-form-item v-if="inbound.xtls" label="Flow">
             <a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
                 <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
                 <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>

+ 1 - 1
web/html/xui/form/protocol/vmess.html

@@ -1,7 +1,7 @@
 {{define "form/vmess"}}
 <a-form layout="inline">
 <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">    
-    <a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
+    <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
         <a-form layout="inline">
             <a-form-item>
                 <span slot="label">

+ 69 - 23
web/html/xui/form/tls_settings.html

@@ -17,7 +17,7 @@
         </span>
         <a-switch v-model="inbound.reality"></a-switch>
     </a-form-item>
-    <a-form-item v-if="inbound.canEnableXTLS()">
+    <a-form-item v-if="inbound.canEnableXtls()">
         <span slot="label">
             XTLS
             <a-tooltip>
@@ -27,14 +27,14 @@
                 <a-icon type="question-circle" theme="filled"></a-icon>
             </a-tooltip>
         </span>
-        <a-switch v-model="inbound.XTLS"></a-switch>
+        <a-switch v-model="inbound.xtls"></a-switch>
     </a-form-item>
 </a-form>
 
 <!-- tls settings -->
-<a-form v-if="inbound.tls || inbound.XTLS" layout="inline">
-    <a-form-item label="SNI" placeholder="Server Name Indication" v-if="inbound.tls">
-        <a-input v-model.trim="inbound.stream.tls.settings[0].serverName"></a-input>
+<a-form v-if="inbound.tls" layout="inline">
+    <a-form-item label='{{ i18n "domainName" }}'>
+        <a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
     </a-form-item>
     <a-form-item label="CipherSuites">
         <a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px">
@@ -52,22 +52,22 @@
             <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
         </a-select>
     </a-form-item>
-    <a-form-item label="uTLS" v-if="inbound.tls" >
-        <a-select v-model="inbound.stream.tls.settings[0].fingerprint" style="width: 135px">
+    <a-form-item label="SNI" placeholder="Server Name Indication">
+        <a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input>
+    </a-form-item>
+    <a-form-item label="uTLS">
+        <a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 170px">
             <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>
     </a-form-item>
-    <a-form-item label='{{ i18n "domainName" }}'>
-        <a-input v-model.trim="inbound.stream.tls.server"></a-input>
-    </a-form-item>
     <a-form-item label="Alpn">
         <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-group>
     </a-form-item>
     <a-form-item label="Allow insecure">
-        <a-switch v-model="inbound.stream.tls.settings[0].allowInsecure"></a-switch>
+        <a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
     </a-form-item>
     <a-form-item label='{{ i18n "certificate" }}'>
         <a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid">
@@ -93,33 +93,79 @@
         </a-form-item>
     </template>
 </a-form>
+
+<!-- xtls settings -->
+<a-form v-if="inbound.xtls" layout="inline">
+    <a-form-item label='{{ i18n "domainName" }}'>
+        <a-input v-model.trim="inbound.stream.xtls.server"></a-input>
+    </a-form-item>
+    <a-form-item label="Alpn">
+        <a-checkbox-group v-model="inbound.stream.xtls.alpn" style="width:200px">
+            <a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox>
+        </a-checkbox-group>
+    </a-form-item>
+    <a-form-item label="Allow insecure">
+        <a-switch v-model="inbound.stream.xtls.settings.allowInsecure"></a-switch>
+    </a-form-item>
+    <a-form-item label='{{ i18n "certificate" }}'>
+        <a-radio-group v-model="inbound.stream.xtls.certs[0].useFile" button-style="solid">
+            <a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
+            <a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
+        </a-radio-group>
+    </a-form-item>
+    <template v-if="inbound.stream.xtls.certs[0].useFile">
+        <a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
+            <a-input v-model.trim="inbound.stream.xtls.certs[0].certFile" style="width:300px;"></a-input>
+        </a-form-item>
+        <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-form-item>
+        <a-button @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
+    </template>
+    <template v-else>
+        <a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
+            <a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.xtls.certs[0].cert"></a-input>
+        </a-form-item>
+        <a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
+            <a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.xtls.certs[0].key"></a-input>
+        </a-form-item>
+    </template>
+</a-form>
+
+<!-- reality settings -->
 <a-form v-else-if="inbound.reality" layout="inline">
-    <a-form-item label="show">
+    <a-form-item label="Show">
         <a-switch v-model="inbound.stream.reality.show">
         </a-switch>
     </a-form-item>
-    <a-form-item label="xver">
+    <a-form-item label="xVer">
         <a-input type="number" v-model.number="inbound.stream.reality.xver" :min="0" style="width: 60px"></a-input>
     </a-form-item>
     <a-form-item label="uTLS" >
-        <a-select v-model="inbound.stream.reality.fingerprint" style="width: 135px">
+        <a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 135px">
             <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
         </a-select>
+    </a-form-item>
+    <a-form-item label='{{ i18n "domainName" }}'>
+        <a-input v-model.trim="inbound.stream.reality.settings.serverName" style="width: 250px"></a-input>
     </a-form-item>
 	<a-form-item label="dest">
-        <a-input v-model.trim="inbound.stream.reality.dest" style="width: 360px"></a-input>
+        <a-input v-model.trim="inbound.stream.reality.dest" style="width: 300px"></a-input>
+    </a-form-item>
+    <a-form-item label="Server Names">
+        <a-input v-model.trim="inbound.stream.reality.serverNames" style="width: 300px"></a-input>
     </a-form-item>
-    <a-form-item label="serverNames">
-        <a-input v-model.trim="inbound.stream.reality.serverNames" style="width: 360px"></a-input>
+    <a-form-item label="ShortIds">
+        <a-input v-model.trim="inbound.stream.reality.shortIds"></a-input>
     </a-form-item>
-    <a-form-item label="privateKey">
-        <a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 360px"></a-input>
+    <a-form-item label="Private Key">
+        <a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 300px"></a-input>
     </a-form-item>
-    <a-form-item label="publicKey">
-        <a-input v-model.trim="inbound.stream.reality.publicKey" style="width: 360px"></a-input>
+    <a-form-item label="Public Key">
+        <a-input v-model.trim="inbound.stream.reality.settings.publicKey" style="width: 300px"></a-input>
     </a-form-item>
-    <a-form-item label="shortIds">
-        <a-input v-model.trim="inbound.stream.reality.shortIds"></a-input>
+    <a-form-item >
+        <a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Key</a-button>
     </a-form-item>
 </a-form>
 {{end}}

+ 5 - 1
web/html/xui/inbound_info_modal.html

@@ -49,10 +49,14 @@
                     tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
                     tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
                 </td>
-                <td v-else-if="inbound.XTLS">
+                <td v-else-if="inbound.xtls">
                     xtls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
                     xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
                 </td>
+                <td v-else-if="inbound.reality">
+                    reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
+                    reality {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
+                </td>
                 <td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
             </td>
         </tr>

+ 34 - 21
web/html/xui/inbound_modal.html

@@ -43,6 +43,14 @@
         loading(loading) {
             inModal.confirmLoading = loading;
         },
+        getClients(protocol, clientSettings) {
+            switch(protocol){
+                case Protocols.VMESS: return clientSettings.vmesses;
+                case Protocols.VLESS: return clientSettings.vlesses;
+                case Protocols.TROJAN: return clientSettings.trojans;
+                default: return null;
+            }
+        },
     };
 
     const protocols = {
@@ -62,6 +70,7 @@
             inModal: inModal,
             Protocols: protocols,
             SSMethods: SSMethods,
+            delayedStart: false,
             get inbound() {
                 return inModal.inbound;
             },
@@ -70,36 +79,40 @@
             },
             get isEdit() {
                 return inModal.isEdit;
-            }
-        },
-        methods: {
-            streamNetworkChange(oldValue) {
-                if (oldValue === 'kcp') {
-                    this.inModal.inbound.tls = false;
-                }
             },
-            addClient(protocol, clients) {
-                switch (protocol) {
-                    case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
-                    case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
-                    case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
-                    default: return null;
-                }
+            get client() {
+                return inModal.getClients(this.inbound.protocol, this.inbound.settings)[0];
             },
-            removeClient(index, clients) {
-                clients.splice(index, 1);
+            get delayedExpireDays() {
+                return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
             },
-            isExpiry(index) {
-                return this.inbound.isExpiry(index)
+            set delayedExpireDays(days){
+                this.client.expiryTime = -86400000 * days;
             },
-            isClientEnable(email) {
-                clientStats = this.dbInbound.clientStats ? this.dbInbound.clientStats.find(stats => stats.email === email) : null
-                return clientStats ? clientStats['enable'] : true
+        },
+        methods: {
+            streamNetworkChange() {
+                if (!inModal.inbound.canSetTls()) {
+                    this.inModal.inbound.stream.security = 'none';
+                }
+                if (!inModal.inbound.canEnableReality()) {
+                    this.inModal.inbound.reality = false;
+                }
             },
             setDefaultCertData(){
                 inModal.inbound.stream.tls.certs[0].certFile = app.defaultCert;
                 inModal.inbound.stream.tls.certs[0].keyFile = app.defaultKey;
             },
+            async getNewX25519Cert(){
+                inModal.loading(true);
+                const msg = await HttpUtil.post('/server/getNewX25519Cert');
+                inModal.loading(false);
+                if (!msg.success) {
+                    return;
+                }
+                inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
+                inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
+            },
             getNewEmail(client) {
                 var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
                 var string = '';

+ 17 - 17
web/html/xui/inbounds.html

@@ -133,26 +133,26 @@
                                 <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
                                     <a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
                                     <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">TLS</a-tag>
-                                    <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXTLS" color="cyan">XTLS</a-tag>
+                                    <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXtls" color="cyan">XTLS</a-tag>
                                     <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="cyan">Reality</a-tag>
                                 </template>
                             </template>
                             <template slot="clients" slot-scope="text, dbInbound">
                                 <template v-if="clientCount[dbInbound.id]">
                                     <a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
-                                    <a-popover title="{{ i18n "disabled" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
+                                    <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
                                         <template slot="content">
                                             <p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
                                         </template>
                                         <a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
                                     </a-popover>
-                                    <a-popover title="{{ i18n "depleted" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
+                                    <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
                                         <template slot="content">
                                             <p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
                                         </template>
                                         <a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
                                     </a-popover>
-                                    <a-popover title="{{ i18n "depletingSoon" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
+                                    <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
                                         <template slot="content">
                                             <p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
                                         </template>
@@ -531,9 +531,9 @@
                     title: '{{ i18n "pages.client.add"}}',
                     okText: '{{ i18n "pages.client.submitAdd"}}',
                     dbInbound: dbInbound,
-                    confirm: async (inbound, dbInbound, index) => {
+                    confirm: async (clients, dbInboundId) => {
                         clientModal.loading();
-                        await this.addClient(inbound, dbInbound);
+                        await this.addClient(clients, dbInboundId);
                         clientModal.close();
                     },
                     isEdit: false
@@ -545,9 +545,9 @@
                     title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark,
                     okText: '{{ i18n "pages.client.bulk"}}',
                     dbInbound: dbInbound,
-                    confirm: async (inbound, dbInbound) => {
+                    confirm: async (clients, dbInboundId) => {
                         clientsBulkModal.loading();
-                        await this.addClient(inbound, dbInbound);
+                        await this.addClient(clients, dbInboundId);
                         clientsBulkModal.close();
                     },
                 });
@@ -561,9 +561,9 @@
                     okText: '{{ i18n "pages.client.submitEdit"}}',
                     dbInbound: dbInbound,
                     index: index,
-                    confirm: async (inbound, dbInbound, index) => {
+                    confirm: async (client, dbInboundId, index) => {
                         clientModal.loading();
-                        await this.updateClient(inbound, dbInbound, index);
+                        await this.updateClient(client, dbInboundId, index);
                         clientModal.close();
                     },
                     isEdit: true
@@ -573,17 +573,17 @@
                 firstKey = Object.keys(client)[0];
                 return clients.findIndex(c => c[firstKey] === client[firstKey]);
             },
-            async addClient(inbound, dbInbound) {
+            async addClient(clients, dbInboundId) {
                 const data = {
-                    id: dbInbound.id,
-                    settings: inbound.settings.toString(),
+                    id: dbInboundId,
+                    settings: '{"clients": [' + clients.toString() +']}',
                 };
-                await this.submit('/xui/inbound/addClient/', data);
+                await this.submit(`/xui/inbound/addClient`, data);
             },
-            async updateClient(inbound, dbInbound, index) {
+            async updateClient(client, dbInboundId, index) {
                 const data = {
-                    id: dbInbound.id,
-                    settings: inbound.settings.toString(),
+                    id: dbInboundId,
+                    settings: '{"clients": [' + client.toString() +']}',
                 };
                 await this.submit(`/xui/inbound/updateClient/${index}`, data);
             },

+ 0 - 18
web/html/xui/setting.html

@@ -125,7 +125,6 @@
                                             <setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigOpenAIWARP"}}' desc='{{ i18n "pages.setting.xrayConfigOpenAIWARPDesc"}}'  v-model="OpenAIWARPSettings"></setting-list-item>
                                             <setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.setting.xrayConfigNetflixWARPDesc"}}'  v-model="NetflixWARPSettings"></setting-list-item>
                                             <setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.setting.xrayConfigSpotifyWARPDesc"}}'  v-model="SpotifyWARPSettings"></setting-list-item>
-                                            <setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigIRWARP"}}' desc='{{ i18n "pages.setting.xrayConfigIRWARPDesc"}}'  v-model="IRWARPSettings"></setting-list-item>
                                         </a-collapse-panel>
                                     </a-collapse>
 
@@ -672,23 +671,6 @@
                         });
                     },
                 },
-                IRWARPSettings: {
-                    get: function () {
-                        return this.templateRuleGetter({
-                            outboundTag: "WARP",
-                            property: "domain",
-                            data: this.settingsData.domains.ir
-                        });
-                    },
-                    set: function (newValue) {
-                        this.templateRuleSetter({
-                            newValue,
-                            outboundTag: "WARP",
-                            property: "domain",
-                            data: this.settingsData.domains.ir
-                        });
-                    },
-                },
             }
         });
 

+ 106 - 61
web/service/inbound.go

@@ -64,28 +64,45 @@ func (s *InboundService) getClients(inbound *model.Inbound) ([]model.Client, err
 	return clients, nil
 }
 
-func (s *InboundService) checkEmailsExist(emails map[string]bool, ignoreId int) (string, error) {
+func (s *InboundService) getAllEmails() ([]string, error) {
 	db := database.GetDB()
-	var inbounds []*model.Inbound
-	db = db.Model(model.Inbound{}).Where("Protocol in ?", []model.Protocol{model.VMess, model.VLESS, model.Trojan})
-	if ignoreId > 0 {
-		db = db.Where("id != ?", ignoreId)
-	}
-	db = db.Find(&inbounds)
-	if db.Error != nil {
-		return "", db.Error
+	var emails []string
+	err := db.Raw(`
+		SELECT JSON_EXTRACT(client.value, '$.email')
+		FROM inbounds,
+			JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
+		`).Scan(&emails).Error
+
+	if err != nil {
+		return nil, err
 	}
+	return emails, nil
+}
 
-	for _, inbound := range inbounds {
-		clients, err := s.getClients(inbound)
-		if err != nil {
-			return "", err
+func (s *InboundService) contains(slice []string, str string) bool {
+	for _, s := range slice {
+		if s == str {
+			return true
 		}
+	}
+	return false
+}
 
-		for _, client := range clients {
-			if emails[client.Email] {
+func (s *InboundService) checkEmailsExistForClients(clients []model.Client) (string, error) {
+	allEmails, err := s.getAllEmails()
+	if err != nil {
+		return "", err
+	}
+	var emails []string
+	for _, client := range clients {
+		if client.Email != "" {
+			if s.contains(emails, client.Email) {
+				return client.Email, nil
+			}
+			if s.contains(allEmails, client.Email) {
 				return client.Email, nil
 			}
+			emails = append(emails, client.Email)
 		}
 	}
 	return "", nil
@@ -96,16 +113,23 @@ func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (stri
 	if err != nil {
 		return "", err
 	}
-	emails := make(map[string]bool)
+	allEmails, err := s.getAllEmails()
+	if err != nil {
+		return "", err
+	}
+	var emails []string
 	for _, client := range clients {
 		if client.Email != "" {
-			if emails[client.Email] {
+			if s.contains(emails, client.Email) {
+				return client.Email, nil
+			}
+			if s.contains(allEmails, client.Email) {
 				return client.Email, nil
 			}
-			emails[client.Email] = true
+			emails = append(emails, client.Email)
 		}
 	}
-	return s.checkEmailsExist(emails, inbound.Id)
+	return "", nil
 }
 
 func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, error) {
@@ -215,14 +239,6 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
 		return inbound, common.NewError("Port already exists:", inbound.Port)
 	}
 
-	existEmail, err := s.checkEmailExistForInbound(inbound)
-	if err != nil {
-		return inbound, err
-	}
-	if existEmail != "" {
-		return inbound, common.NewError("Duplicate email:", existEmail)
-	}
-
 	oldInbound, err := s.GetInbound(inbound.Id)
 	if err != nil {
 		return inbound, err
@@ -245,8 +261,12 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
 	return inbound, db.Save(oldInbound).Error
 }
 
-func (s *InboundService) AddInboundClient(inbound *model.Inbound) error {
-	existEmail, err := s.checkEmailExistForInbound(inbound)
+func (s *InboundService) AddInboundClient(data *model.Inbound) error {
+	clients, err := s.getClients(data)
+	if err != nil {
+		return err
+	}
+	existEmail, err := s.checkEmailsExistForClients(clients)
 	if err != nil {
 		return err
 	}
@@ -255,29 +275,35 @@ func (s *InboundService) AddInboundClient(inbound *model.Inbound) error {
 		return common.NewError("Duplicate email:", existEmail)
 	}
 
-	clients, err := s.getClients(inbound)
+	oldInbound, err := s.GetInbound(data.Id)
 	if err != nil {
 		return err
 	}
 
-	oldInbound, err := s.GetInbound(inbound.Id)
+	var settings map[string]interface{}
+	err = json.Unmarshal([]byte(oldInbound.Settings), &settings)
 	if err != nil {
 		return err
 	}
 
-	oldClients, err := s.getClients(oldInbound)
+	oldClients := settings["clients"].([]interface{})
+	var newClients []interface{}
+	for _, client := range clients {
+		newClients = append(newClients, client)
+	}
+
+	settings["clients"] = append(oldClients, newClients...)
+
+	newSettings, err := json.MarshalIndent(settings, "", "  ")
 	if err != nil {
 		return err
 	}
 
-	oldInbound.Settings = inbound.Settings
+	oldInbound.Settings = string(newSettings)
 
-	if len(clients[len(clients)-1].Email) > 0 {
-		s.AddClientStat(inbound.Id, &clients[len(clients)-1])
-	}
-	for i := len(oldClients); i < len(clients); i++ {
-		if len(clients[i].Email) > 0 {
-			s.AddClientStat(inbound.Id, &clients[i])
+	for _, client := range clients {
+		if len(client.Email) > 0 {
+			s.AddClientStat(data.Id, &client)
 		}
 	}
 	db := database.GetDB()
@@ -309,37 +335,56 @@ func (s *InboundService) DelInboundClient(inbound *model.Inbound, email string)
 	return db.Save(oldInbound).Error
 }
 
-func (s *InboundService) UpdateInboundClient(inbound *model.Inbound, index int) error {
-	existEmail, err := s.checkEmailExistForInbound(inbound)
+func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) error {
+	clients, err := s.getClients(data)
 	if err != nil {
 		return err
 	}
-	if existEmail != "" {
-		return common.NewError("Duplicate email:", existEmail)
-	}
 
-	clients, err := s.getClients(inbound)
+	oldInbound, err := s.GetInbound(data.Id)
 	if err != nil {
 		return err
 	}
 
-	oldInbound, err := s.GetInbound(inbound.Id)
+	oldClients, err := s.getClients(oldInbound)
 	if err != nil {
 		return err
 	}
 
-	oldClients, err := s.getClients(oldInbound)
+	if len(clients[0].Email) > 0 && clients[0].Email != oldClients[index].Email {
+		existEmail, err := s.checkEmailsExistForClients(clients)
+		if err != nil {
+			return err
+		}
+		if existEmail != "" {
+			return common.NewError("Duplicate email:", existEmail)
+		}
+	}
+
+	var settings map[string]interface{}
+	err = json.Unmarshal([]byte(oldInbound.Settings), &settings)
 	if err != nil {
 		return err
 	}
 
-	oldInbound.Settings = inbound.Settings
+	settingsClients := settings["clients"].([]interface{})
+	var newClients []interface{}
+	newClients = append(newClients, clients[0])
+	settingsClients[index] = newClients[0]
+
+	settings["clients"] = settingsClients
+
+	newSettings, err := json.MarshalIndent(settings, "", "  ")
+	if err != nil {
+		return err
+	}
 
+	oldInbound.Settings = string(newSettings)
 	db := database.GetDB()
 
-	if len(clients[index].Email) > 0 {
+	if len(clients[0].Email) > 0 {
 		if len(oldClients[index].Email) > 0 {
-			err = s.UpdateClientStat(oldClients[index].Email, &clients[index])
+			err = s.UpdateClientStat(oldClients[index].Email, &clients[0])
 			if err != nil {
 				return err
 			}
@@ -348,7 +393,7 @@ func (s *InboundService) UpdateInboundClient(inbound *model.Inbound, index int)
 				return err
 			}
 		} else {
-			s.AddClientStat(inbound.Id, &clients[index])
+			s.AddClientStat(data.Id, &clients[0])
 		}
 	} else {
 		err = s.DelClientStat(db, oldClients[index].Email)
@@ -507,6 +552,16 @@ func (s *InboundService) DisableInvalidInbounds() (int64, error) {
 	count := result.RowsAffected
 	return count, err
 }
+func (s *InboundService) DisableInvalidClients() (int64, error) {
+	db := database.GetDB()
+	now := time.Now().Unix() * 1000
+	result := db.Model(xray.ClientTraffic{}).
+		Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
+		Update("enable", false)
+	err := result.Error
+	count := result.RowsAffected
+	return count, err
+}
 func (s *InboundService) RemoveOrphanedTraffics() {
 	db := database.GetDB()
 	db.Exec(`
@@ -518,16 +573,6 @@ func (s *InboundService) RemoveOrphanedTraffics() {
 		)
 	`)
 }
-func (s *InboundService) DisableInvalidClients() (int64, error) {
-	db := database.GetDB()
-	now := time.Now().Unix() * 1000
-	result := db.Model(xray.ClientTraffic{}).
-		Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
-		Update("enable", false)
-	err := result.Error
-	count := result.RowsAffected
-	return count, err
-}
 func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
 	db := database.GetDB()
 

+ 26 - 0
web/service/server.go

@@ -390,3 +390,29 @@ func (s *ServerService) GetDb() ([]byte, error) {
 
 	return fileContents, nil
 }
+
+func (s *ServerService) GetNewX25519Cert() (interface{}, error) {
+	// Run the command
+	cmd := exec.Command(xray.GetBinaryPath(), "x25519")
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	if err != nil {
+		return nil, err
+	}
+
+	lines := strings.Split(out.String(), "\n")
+
+	privateKeyLine := strings.Split(lines[0], ":")
+	publicKeyLine := strings.Split(lines[1], ":")
+
+	privateKey := strings.TrimSpace(privateKeyLine[1])
+	publicKey := strings.TrimSpace(publicKeyLine[1])
+
+	keyPair := map[string]interface{}{
+		"privateKey": privateKey,
+		"publicKey":  publicKey,
+	}
+
+	return keyPair, nil
+}

+ 56 - 14
web/service/sub.go

@@ -8,6 +8,7 @@ import (
 	"x-ui/database"
 	"x-ui/database/model"
 	"x-ui/logger"
+	"x-ui/xray"
 
 	"github.com/goccy/go-json"
 	"gorm.io/gorm"
@@ -18,12 +19,15 @@ type SubService struct {
 	inboundService InboundService
 }
 
-func (s *SubService) GetSubs(subId string, host string) ([]string, error) {
+func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
 	s.address = host
 	var result []string
+	var header string
+	var traffic xray.ClientTraffic
+	var clientTraffics []xray.ClientTraffic
 	inbounds, err := s.getInboundsBySubId(subId)
 	if err != nil {
-		return nil, err
+		return nil, "", err
 	}
 	for _, inbound := range inbounds {
 		clients, err := s.inboundService.getClients(inbound)
@@ -37,22 +41,60 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, error) {
 			if client.SubID == subId {
 				link := s.getLink(inbound, client.Email)
 				result = append(result, link)
+				clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
+			}
+		}
+	}
+	for index, clientTraffic := range clientTraffics {
+		if index == 0 {
+			traffic.Up = clientTraffic.Up
+			traffic.Down = clientTraffic.Down
+			traffic.Total = clientTraffic.Total
+			if clientTraffic.ExpiryTime > 0 {
+				traffic.ExpiryTime = clientTraffic.ExpiryTime
+			}
+		} else {
+			traffic.Up += clientTraffic.Up
+			traffic.Down += clientTraffic.Down
+			if traffic.Total == 0 || clientTraffic.Total == 0 {
+				traffic.Total = 0
+			} else {
+				traffic.Total += clientTraffic.Total
+			}
+			if clientTraffic.ExpiryTime != traffic.ExpiryTime {
+				traffic.ExpiryTime = 0
 			}
 		}
 	}
-	return result, nil
+	header = fmt.Sprintf("upload=%d;download=%d", traffic.Up, traffic.Down)
+	if traffic.Total > 0 {
+		header = header + fmt.Sprintf(";total=%d", traffic.Total)
+	}
+	if traffic.ExpiryTime > 0 {
+		header = header + fmt.Sprintf(";expire=%d", traffic.ExpiryTime)
+	}
+	return result, header, nil
 }
 
 func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
 	db := database.GetDB()
 	var inbounds []*model.Inbound
-	err := db.Model(model.Inbound{}).Where("settings like ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId)).Find(&inbounds).Error
+	err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId)).Find(&inbounds).Error
 	if err != nil && err != gorm.ErrRecordNotFound {
 		return nil, err
 	}
 	return inbounds, nil
 }
 
+func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email string) xray.ClientTraffic {
+	for _, traffic := range traffics {
+		if traffic.Email == email {
+			return traffic
+		}
+	}
+	return xray.ClientTraffic{}
+}
+
 func (s *SubService) getLink(inbound *model.Inbound, email string) string {
 	switch inbound.Protocol {
 	case "vmess":
@@ -296,7 +338,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
 
 	if security == "xtls" {
 		params["security"] = "xtls"
-		xtlsSetting, _ := stream["XTLSSettings"].(map[string]interface{})
+		xtlsSetting, _ := stream["xtlsSettings"].(map[string]interface{})
 		alpns, _ := xtlsSetting["alpn"].([]interface{})
 		var alpn []string
 		for _, a := range alpns {
@@ -306,15 +348,15 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
 			params["alpn"] = strings.Join(alpn, ",")
 		}
 
-		XTLSSettings, _ := searchKey(xtlsSetting, "settings")
+		xtlsSettings, _ := searchKey(xtlsSetting, "settings")
 		if xtlsSetting != nil {
-			if sniValue, ok := searchKey(XTLSSettings, "serverName"); ok {
+			if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
 				params["sni"], _ = sniValue.(string)
 			}
-			if fpValue, ok := searchKey(XTLSSettings, "fingerprint"); ok {
+			if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
 				params["fp"], _ = fpValue.(string)
 			}
-			if insecure, ok := searchKey(XTLSSettings, "allowInsecure"); ok {
+			if insecure, ok := searchKey(xtlsSettings, "allowInsecure"); ok {
 				if insecure.(bool) {
 					params["allowInsecure"] = "1"
 				}
@@ -465,7 +507,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
 
 	if security == "xtls" {
 		params["security"] = "xtls"
-		xtlsSetting, _ := stream["XTLSSettings"].(map[string]interface{})
+		xtlsSetting, _ := stream["xtlsSettings"].(map[string]interface{})
 		alpns, _ := xtlsSetting["alpn"].([]interface{})
 		var alpn []string
 		for _, a := range alpns {
@@ -475,15 +517,15 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
 			params["alpn"] = strings.Join(alpn, ",")
 		}
 
-		XTLSSettings, _ := searchKey(xtlsSetting, "settings")
+		xtlsSettings, _ := searchKey(xtlsSetting, "settings")
 		if xtlsSetting != nil {
-			if sniValue, ok := searchKey(XTLSSettings, "serverName"); ok {
+			if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
 				params["sni"], _ = sniValue.(string)
 			}
-			if fpValue, ok := searchKey(XTLSSettings, "fingerprint"); ok {
+			if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
 				params["fp"], _ = fpValue.(string)
 			}
-			if insecure, ok := searchKey(XTLSSettings, "allowInsecure"); ok {
+			if insecure, ok := searchKey(xtlsSettings, "allowInsecure"); ok {
 				if insecure.(bool) {
 					params["allowInsecure"] = "1"
 				}