Browse Source

Expand multiDomain to externalProxy #1300

Alireza Ahmadi 1 year ago
parent
commit
5fbf8f0d53

+ 191 - 97
sub/subService.go

@@ -53,6 +53,7 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
 				json.Unmarshal([]byte(fallbackMaster.StreamSettings), &masterStream)
 				stream["security"] = masterStream["security"]
 				stream["tlsSettings"] = masterStream["tlsSettings"]
+				stream["externalProxy"] = masterStream["externalProxy"]
 				modifiedStream, _ := json.MarshalIndent(stream, "", "  ")
 				inbound.StreamSettings = string(modifiedStream)
 			}
@@ -96,7 +97,14 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
 func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
 	db := database.GetDB()
 	var inbounds []*model.Inbound
-	err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ? and enable = ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId), true).Find(&inbounds).Error
+	err := db.Model(model.Inbound{}).Preload("ClientStats").Where(`id in (
+		SELECT DISTINCT inbounds.id
+		FROM inbounds,
+			JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client 
+		WHERE
+			protocol in ('vmess','vless','trojan','shadowsocks')
+			AND JSON_EXTRACT(client.value, '$.subId') = ? AND enable = ?
+	)`, subId, true).Find(&inbounds).Error
 	if err != nil {
 		return nil, err
 	}
@@ -196,7 +204,6 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
 	}
 
 	security, _ := stream["security"].(string)
-	var domains []interface{}
 	obj["tls"] = security
 	if security == "tls" {
 		tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
@@ -208,24 +215,18 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
 			}
 			obj["alpn"] = strings.Join(alpn, ",")
 		}
+		if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
+			obj["sni"], _ = sniValue.(string)
+		}
+
 		tlsSettings, _ := searchKey(tlsSetting, "settings")
 		if tlsSetting != nil {
-			if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
-				obj["sni"], _ = sniValue.(string)
-			}
 			if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
 				obj["fp"], _ = fpValue.(string)
 			}
 			if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
 				obj["allowInsecure"], _ = insecure.(bool)
 			}
-			if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
-				domains, _ = domainSettings.([]interface{})
-			}
-		}
-		serverName, _ := tlsSetting["serverName"].(string)
-		if serverName != "" {
-			obj["add"] = serverName
 		}
 	}
 
@@ -239,16 +240,30 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
 	}
 	obj["id"] = clients[clientIndex].ID
 
-	if len(domains) > 0 {
+	externalProxies, _ := stream["externalProxy"].([]interface{})
+
+	if len(externalProxies) > 0 {
 		links := ""
-		for index, d := range domains {
-			domain := d.(map[string]interface{})
-			obj["ps"] = s.genRemark(inbound, email, domain["remark"].(string))
-			obj["add"] = domain["domain"].(string)
+		for index, externalProxy := range externalProxies {
+			ep, _ := externalProxy.(map[string]interface{})
+			newSecurity, _ := ep["forceTls"].(string)
+			newObj := map[string]interface{}{}
+			for key, value := range obj {
+				if !(newSecurity == "none" && (key == "alpn" || key == "sni" || key == "fp" || key == "allowInsecure")) {
+					newObj[key] = value
+				}
+			}
+			newObj["ps"] = s.genRemark(inbound, email, ep["remark"].(string))
+			newObj["add"] = ep["dest"].(string)
+			newObj["port"] = int(ep["port"].(float64))
+
+			if newSecurity != "same" {
+				newObj["tls"] = newSecurity
+			}
 			if index > 0 {
 				links += "\n"
 			}
-			jsonStr, _ := json.MarshalIndent(obj, "", "  ")
+			jsonStr, _ := json.MarshalIndent(newObj, "", "  ")
 			links += "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
 		}
 		return links
@@ -323,7 +338,6 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
 	}
 
 	security, _ := stream["security"].(string)
-	var domains []interface{}
 	if security == "tls" {
 		params["security"] = "tls"
 		tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
@@ -335,11 +349,12 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
 		if len(alpn) > 0 {
 			params["alpn"] = strings.Join(alpn, ",")
 		}
+		if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
+			params["sni"], _ = sniValue.(string)
+		}
+
 		tlsSettings, _ := searchKey(tlsSetting, "settings")
 		if tlsSetting != nil {
-			if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
-				params["sni"], _ = sniValue.(string)
-			}
 			if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
 				params["fp"], _ = fpValue.(string)
 			}
@@ -348,19 +363,11 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
 					params["allowInsecure"] = "1"
 				}
 			}
-			if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
-				domains, _ = domainSettings.([]interface{})
-			}
 		}
 
 		if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
 			params["flow"] = clients[clientIndex].Flow
 		}
-
-		serverName, _ := tlsSetting["serverName"].(string)
-		if serverName != "" {
-			address = serverName
-		}
 	}
 
 	if security == "reality" {
@@ -389,11 +396,6 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
 					params["spx"] = spx
 				}
 			}
-			if serverName, ok := searchKey(realitySettings, "serverName"); ok {
-				if sname, ok := serverName.(string); ok && len(sname) > 0 {
-					address = sname
-				}
-			}
 		}
 
 		if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
@@ -412,7 +414,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
 		if len(alpn) > 0 {
 			params["alpn"] = strings.Join(alpn, ",")
 		}
-
+		if sniValue, ok := searchKey(xtlsSetting, "serverName"); ok {
+			params["sni"], _ = sniValue.(string)
+		}
 		xtlsSettings, _ := searchKey(xtlsSetting, "settings")
 		if xtlsSetting != nil {
 			if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
@@ -423,42 +427,47 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
 					params["allowInsecure"] = "1"
 				}
 			}
-			if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
-				params["sni"], _ = sniValue.(string)
-			}
 		}
 
 		if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
 			params["flow"] = clients[clientIndex].Flow
 		}
-
-		serverName, _ := xtlsSetting["serverName"].(string)
-		if serverName != "" {
-			address = serverName
-		}
 	}
 
 	if security != "tls" && security != "reality" && security != "xtls" {
 		params["security"] = "none"
 	}
 
-	link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)
-	url, _ := url.Parse(link)
-	q := url.Query()
+	externalProxies, _ := stream["externalProxy"].([]interface{})
 
-	for k, v := range params {
-		q.Add(k, v)
-	}
+	if len(externalProxies) > 0 {
+		links := ""
+		for index, externalProxy := range externalProxies {
+			ep, _ := externalProxy.(map[string]interface{})
+			newSecurity, _ := ep["forceTls"].(string)
+			dest, _ := ep["dest"].(string)
+			port := int(ep["port"].(float64))
+			link := fmt.Sprintf("vless://%s@%s:%d", uuid, dest, port)
+
+			if newSecurity != "same" {
+				params["security"] = newSecurity
+			} else {
+				params["security"] = security
+			}
+			url, _ := url.Parse(link)
+			q := url.Query()
 
-	// Set the new query values on the URL
-	url.RawQuery = q.Encode()
+			for k, v := range params {
+				if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
+					q.Add(k, v)
+				}
+			}
+
+			// Set the new query values on the URL
+			url.RawQuery = q.Encode()
+
+			url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
 
-	if len(domains) > 0 {
-		links := ""
-		for index, d := range domains {
-			domain := d.(map[string]interface{})
-			url.Fragment = s.genRemark(inbound, email, domain["remark"].(string))
-			url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
 			if index > 0 {
 				links += "\n"
 			}
@@ -467,6 +476,17 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
 		return links
 	}
 
+	link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)
+	url, _ := url.Parse(link)
+	q := url.Query()
+
+	for k, v := range params {
+		q.Add(k, v)
+	}
+
+	// Set the new query values on the URL
+	url.RawQuery = q.Encode()
+
 	url.Fragment = s.genRemark(inbound, email, "")
 	return url.String()
 }
@@ -534,7 +554,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
 	}
 
 	security, _ := stream["security"].(string)
-	var domains []interface{}
 	if security == "tls" {
 		params["security"] = "tls"
 		tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
@@ -546,11 +565,11 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
 		if len(alpn) > 0 {
 			params["alpn"] = strings.Join(alpn, ",")
 		}
+		if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
+			params["sni"], _ = sniValue.(string)
+		}
 		tlsSettings, _ := searchKey(tlsSetting, "settings")
 		if tlsSetting != nil {
-			if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
-				params["sni"], _ = sniValue.(string)
-			}
 			if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
 				params["fp"], _ = fpValue.(string)
 			}
@@ -559,14 +578,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
 					params["allowInsecure"] = "1"
 				}
 			}
-			if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
-				domains, _ = domainSettings.([]interface{})
-			}
-		}
-
-		serverName, _ := tlsSetting["serverName"].(string)
-		if serverName != "" {
-			address = serverName
 		}
 	}
 
@@ -596,11 +607,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
 					params["spx"] = spx
 				}
 			}
-			if serverName, ok := searchKey(realitySettings, "serverName"); ok {
-				if sname, ok := serverName.(string); ok && len(sname) > 0 {
-					address = sname
-				}
-			}
 		}
 
 		if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
@@ -619,6 +625,9 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
 		if len(alpn) > 0 {
 			params["alpn"] = strings.Join(alpn, ",")
 		}
+		if sniValue, ok := searchKey(xtlsSetting, "serverName"); ok {
+			params["sni"], _ = sniValue.(string)
+		}
 
 		xtlsSettings, _ := searchKey(xtlsSetting, "settings")
 		if xtlsSetting != nil {
@@ -630,43 +639,47 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
 					params["allowInsecure"] = "1"
 				}
 			}
-			if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
-				params["sni"], _ = sniValue.(string)
-			}
 		}
 
 		if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
 			params["flow"] = clients[clientIndex].Flow
 		}
-
-		serverName, _ := xtlsSetting["serverName"].(string)
-		if serverName != "" {
-			address = serverName
-		}
 	}
 
 	if security != "tls" && security != "reality" && security != "xtls" {
 		params["security"] = "none"
 	}
 
-	link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port)
+	externalProxies, _ := stream["externalProxy"].([]interface{})
 
-	url, _ := url.Parse(link)
-	q := url.Query()
+	if len(externalProxies) > 0 {
+		links := ""
+		for index, externalProxy := range externalProxies {
+			ep, _ := externalProxy.(map[string]interface{})
+			newSecurity, _ := ep["forceTls"].(string)
+			dest, _ := ep["dest"].(string)
+			port := int(ep["port"].(float64))
+			link := fmt.Sprintf("trojan://%s@%s:%d", password, dest, port)
+
+			if newSecurity != "same" {
+				params["security"] = newSecurity
+			} else {
+				params["security"] = security
+			}
+			url, _ := url.Parse(link)
+			q := url.Query()
 
-	for k, v := range params {
-		q.Add(k, v)
-	}
+			for k, v := range params {
+				if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
+					q.Add(k, v)
+				}
+			}
 
-	// Set the new query values on the URL
-	url.RawQuery = q.Encode()
+			// Set the new query values on the URL
+			url.RawQuery = q.Encode()
+
+			url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
 
-	if len(domains) > 0 {
-		links := ""
-		for index, d := range domains {
-			domain := d.(map[string]interface{})
-			url.Fragment = s.genRemark(inbound, email, domain["remark"].(string))
-			url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
 			if index > 0 {
 				links += "\n"
 			}
@@ -675,6 +688,18 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
 		return links
 	}
 
+	link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port)
+
+	url, _ := url.Parse(link)
+	q := url.Query()
+
+	for k, v := range params {
+		q.Add(k, v)
+	}
+
+	// Set the new query values on the URL
+	url.RawQuery = q.Encode()
+
 	url.Fragment = s.genRemark(inbound, email, "")
 	return url.String()
 }
@@ -744,10 +769,78 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
 		}
 	}
 
+	security, _ := stream["security"].(string)
+	if security == "tls" {
+		params["security"] = "tls"
+		tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
+		alpns, _ := tlsSetting["alpn"].([]interface{})
+		var alpn []string
+		for _, a := range alpns {
+			alpn = append(alpn, a.(string))
+		}
+		if len(alpn) > 0 {
+			params["alpn"] = strings.Join(alpn, ",")
+		}
+		if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
+			params["sni"], _ = sniValue.(string)
+		}
+
+		tlsSettings, _ := searchKey(tlsSetting, "settings")
+		if tlsSetting != nil {
+			if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
+				params["fp"], _ = fpValue.(string)
+			}
+			if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
+				if insecure.(bool) {
+					params["allowInsecure"] = "1"
+				}
+			}
+		}
+	}
+
 	encPart := fmt.Sprintf("%s:%s", method, clients[clientIndex].Password)
 	if method[0] == '2' {
 		encPart = fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
 	}
+
+	externalProxies, _ := stream["externalProxy"].([]interface{})
+
+	if len(externalProxies) > 0 {
+		links := ""
+		for index, externalProxy := range externalProxies {
+			ep, _ := externalProxy.(map[string]interface{})
+			newSecurity, _ := ep["forceTls"].(string)
+			dest, _ := ep["dest"].(string)
+			port := int(ep["port"].(float64))
+			link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), dest, port)
+
+			if newSecurity != "same" {
+				params["security"] = newSecurity
+			} else {
+				params["security"] = security
+			}
+			url, _ := url.Parse(link)
+			q := url.Query()
+
+			for k, v := range params {
+				if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
+					q.Add(k, v)
+				}
+			}
+
+			// Set the new query values on the URL
+			url.RawQuery = q.Encode()
+
+			url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
+
+			if index > 0 {
+				links += "\n"
+			}
+			links += url.String()
+		}
+		return links
+	}
+
 	link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port)
 	url, _ := url.Parse(link)
 	q := url.Query()
@@ -758,6 +851,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
 
 	// Set the new query values on the URL
 	url.RawQuery = q.Encode()
+
 	url.Fragment = s.genRemark(inbound, email, "")
 	return url.String()
 }

+ 126 - 129
web/assets/js/model/xray.js

@@ -578,27 +578,21 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
 };
 
 TlsStreamSettings.Settings = class extends XrayCommonClass {
-    constructor(allowInsecure = false, fingerprint = '', serverName = '', domains = []) {
+    constructor(allowInsecure = false, fingerprint = '') {
         super();
         this.allowInsecure = allowInsecure;
         this.fingerprint = fingerprint;
-        this.serverName = serverName;
-        this.domains = domains;
     }
     static fromJson(json = {}) {
         return new TlsStreamSettings.Settings(
             json.allowInsecure,
             json.fingerprint,
-            json.serverName,
-            json.domains,
         );
     }
     toJson() {
         return {
             allowInsecure: this.allowInsecure,
             fingerprint: this.fingerprint,
-            serverName: this.serverName,
-            domains: this.domains,
         };
     }
 };
@@ -692,21 +686,18 @@ XtlsStreamSettings.Cert = class extends XrayCommonClass {
 };
 
 XtlsStreamSettings.Settings = class extends XrayCommonClass {
-    constructor(allowInsecure = false, serverName = '') {
+    constructor(allowInsecure = false) {
         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,
         };
     }
 };
@@ -773,18 +764,16 @@ class RealityStreamSettings extends XrayCommonClass {
 }
 
 RealityStreamSettings.Settings = class extends XrayCommonClass {
-    constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, serverName = '', spiderX= '/') {
+    constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, spiderX= '/') {
         super();
         this.publicKey = publicKey;
         this.fingerprint = fingerprint;
-        this.serverName = serverName;
         this.spiderX = spiderX;
     }
     static fromJson(json = {}) {
         return new RealityStreamSettings.Settings(
             json.publicKey,
             json.fingerprint,
-            json.serverName,
             json.spiderX,
         );
     }
@@ -792,7 +781,6 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
         return {
             publicKey: this.publicKey,
             fingerprint: this.fingerprint,
-            serverName: this.serverName,
             spiderX: this.spiderX,
         };
     }
@@ -829,6 +817,7 @@ class SockoptStreamSettings extends XrayCommonClass {
 class StreamSettings extends XrayCommonClass {
     constructor(network='tcp',
         security='none',
+        externalProxy = [],
         tlsSettings=new TlsStreamSettings(),
         xtlsSettings=new XtlsStreamSettings(),
         realitySettings = new RealityStreamSettings(),
@@ -843,6 +832,7 @@ class StreamSettings extends XrayCommonClass {
         super();
         this.network = network;
         this.security = security;
+        this.externalProxy = externalProxy;
         this.tls = tlsSettings;
         this.xtls = xtlsSettings;
         this.reality = realitySettings;
@@ -901,10 +891,10 @@ class StreamSettings extends XrayCommonClass {
     }
 
     static fromJson(json={}) {
-
         return new StreamSettings(
             json.network,
             json.security,
+            json.externalProxy,
             TlsStreamSettings.fromJson(json.tlsSettings),
             XtlsStreamSettings.fromJson(json.xtlsSettings),
             RealityStreamSettings.fromJson(json.realitySettings),
@@ -923,6 +913,7 @@ class StreamSettings extends XrayCommonClass {
         return {
             network: network,
             security: this.security,
+            externalProxy: this.externalProxy,
             tlsSettings: this.isTls ? this.tls.toJson() : undefined,
             xtlsSettings: this.isXtls ? this.xtls.toJson() : undefined,
             realitySettings: this.isReality ? this.reality.toJson() : undefined,
@@ -982,6 +973,16 @@ class Inbound extends XrayCommonClass {
         return this.clientStats;
     }
 
+    get clients() {
+        switch (this.protocol) {
+            case Protocols.VMESS: return this.settings.vmesses;
+            case Protocols.VLESS: return this.settings.vlesses;
+            case Protocols.TROJAN: return this.settings.trojans;
+            case Protocols.SHADOWSOCKS: return this.isSSMultiUser ? this.settings.shadowsockses : null;
+            default: return null;
+        }
+    }
+
     get protocol() {
         return this._protocol;
     }
@@ -1132,26 +1133,8 @@ class Inbound extends XrayCommonClass {
     }
 
     isExpiry(index) {
-        switch (this.protocol) {
-            case Protocols.VMESS:
-                if(this.settings.vmesses[index].expiryTime > 0)
-                    return this.settings.vmesses[index].expiryTime < new Date().getTime();
-                return false
-            case Protocols.VLESS:
-                if(this.settings.vlesses[index].expiryTime > 0)
-                    return this.settings.vlesses[index].expiryTime < new Date().getTime();
-                return false
-                case Protocols.TROJAN:
-                if(this.settings.trojans[index].expiryTime > 0)
-                    return this.settings.trojans[index].expiryTime < new Date().getTime();
-                return false
-            case Protocols.SHADOWSOCKS:
-                if(this.settings.shadowsockses.length > 0 && this.settings.shadowsockses[index].expiryTime > 0)
-                    return this.settings.shadowsockses[index].expiryTime < new Date().getTime();
-                return false
-            default:
-                return false;
-        }
+        let exp = this.clients[index].expiryTime;
+        return exp > 0 ? exp < new Date().getTime() : false;
     }
 
     canEnableTls() {
@@ -1195,19 +1178,20 @@ class Inbound extends XrayCommonClass {
         this.sniffing = new Sniffing();
     }
 
-    genVmessLink(address='', remark='', clientIndex=0) {
+    genVmessLink(address='', port=this.port, forceTls, remark='', clientId) {
         if (this.protocol !== Protocols.VMESS) {
             return '';
         }
+        const security = forceTls == 'same' ? this.stream.security : forceTls;
         let obj = {
             v: '2',
             ps: remark,
             add: address,
-            port: this.port,
-            id: this.settings.vmesses[clientIndex].id,
+            port: port,
+            id: clientId,
             net: this.stream.network,
             type: 'none',
-            tls: this.stream.security,
+            tls: security,
         };
         let network = this.stream.network;
         if (network === 'tcp') {
@@ -1247,12 +1231,9 @@ class Inbound extends XrayCommonClass {
             }
         }
 
-        if (this.stream.security === 'tls') {
-            if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
-                obj.add = this.stream.tls.server;
-            }
-            if (!ObjectUtil.isEmpty(this.stream.tls.settings.serverName)){
-                obj.sni = this.stream.tls.settings.serverName;
+        if (security === 'tls') {
+            if (!ObjectUtil.isEmpty(this.stream.tls.sni)){
+                obj.sni = this.stream.tls.server;
             }
             if (!ObjectUtil.isEmpty(this.stream.tls.settings.fingerprint)){
                 obj.fp = this.stream.tls.settings.fingerprint;
@@ -1268,11 +1249,10 @@ class Inbound extends XrayCommonClass {
         return 'vmess://' + base64(JSON.stringify(obj, null, 2));
     }
 
-    genVLESSLink(address = '', remark='', clientIndex=0) {
-        const settings = this.settings;
-        const uuid = settings.vlesses[clientIndex].id;
-        const port = this.port;
+    genVLESSLink(address = '', port=this.port, forceTls, remark='', clientId, flow) {
+        const uuid = clientId;
         const type = this.stream.network;
+        const security = forceTls == 'same' ? this.stream.security : forceTls;
         const params = new Map();
         params.set("type", this.stream.network);
         switch (type) {
@@ -1323,58 +1303,51 @@ class Inbound extends XrayCommonClass {
                 break;
         }
 
-        if (this.tls) {
+        if (security === 'tls') {
             params.set("security", "tls");
-            params.set("fp" , this.stream.tls.settings.fingerprint);
-            params.set("alpn", this.stream.tls.alpn);
-            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.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.stream.isTls){
+                params.set("fp" , this.stream.tls.settings.fingerprint);
+                params.set("alpn", this.stream.tls.alpn);
+                if(this.stream.tls.settings.allowInsecure){
+                    params.set("allowInsecure", "1");
+                }
+                if (!ObjectUtil.isEmpty(this.stream.tls.server)){
+                    params.set("sni", this.stream.tls.server);
+                }
+                if (type == "tcp" && !ObjectUtil.isEmpty(flow)) {
+                    params.set("flow", flow);
+                }
             }
         }
 
-        else if (this.xtls) {
+        else if (security === 'xtls') {
             params.set("security", "xtls");
             params.set("alpn", this.stream.xtls.alpn);
             if(this.stream.xtls.settings.allowInsecure){
                 params.set("allowInsecure", "1");
             }
-            if (!ObjectUtil.isEmpty(this.stream.xtls.server)) {
-                address = this.stream.xtls.server;
-			}
-            if (this.stream.xtls.settings.serverName !== ''){
-                params.set("sni", this.stream.xtls.settings.serverName);
+            if (!ObjectUtil.isEmpty(this.stream.xtls.server)){
+                params.set("sni", this.stream.xtls.server);
 			}
             params.set("flow", this.settings.vlesses[clientIndex].flow);
         }
 
-        else if (this.reality) {
+        else if (security === 'reality') {
             params.set("security", "reality");
-            params.set("fp", this.stream.reality.settings.fingerprint);
             params.set("pbk", this.stream.reality.settings.publicKey);
+            params.set("fp", this.stream.reality.settings.fingerprint);
             if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
                 params.set("sni", this.stream.reality.serverNames.split(",")[0]);
             }
-            if (this.stream.network === 'tcp' && !ObjectUtil.isEmpty(this.settings.vlesses[clientIndex].flow)) {
-                params.set("flow", this.settings.vlesses[clientIndex].flow);
-            }
             if (this.stream.reality.shortIds.length > 0) {
                 params.set("sid", this.stream.reality.shortIds.split(",")[0]);
             }
-            if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
-                address = this.stream.reality.settings.serverName;
-            }
             if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
                 params.set("spx", this.stream.reality.settings.spiderX);
             }
+            if (type == 'tcp' && !ObjectUtil.isEmpty(flow)) {
+                params.set("flow", flow);
+            }
         }
 
         else {
@@ -1390,10 +1363,10 @@ class Inbound extends XrayCommonClass {
         return url.toString();
     }
 
-    genSSLink(address='', remark='', clientIndex = 0) {
+    genSSLink(address='', port=this.port, forceTls, remark='', clientPassword) {
         let settings = this.settings;
-        const port = this.port;
         const type = this.stream.network;
+        const security = forceTls == 'same' ? this.stream.security : forceTls;
         const params = new Map();
         params.set("type", this.stream.network);
         switch (type) {
@@ -1444,11 +1417,26 @@ class Inbound extends XrayCommonClass {
                 break;
         }
 
+        if (security === 'tls') {
+            params.set("security", "tls");
+            if (this.stream.isTls){
+                params.set("fp" , this.stream.tls.settings.fingerprint);
+                params.set("alpn", this.stream.tls.alpn);
+                if(this.stream.tls.settings.allowInsecure){
+                    params.set("allowInsecure", "1");
+                }
+                if (!ObjectUtil.isEmpty(this.stream.tls.server)){
+                    params.set("sni", this.stream.tls.server);
+                }
+            }
+        }
+
+
         let password = new Array();
         if (this.isSS2022) password.push(settings.password);
-        if (this.isSSMultiUser) password.push(settings.shadowsockses[clientIndex].password);
+        if (this.isSSMultiUser) password.push(clientPassword);
 
-        let link = `ss://${safeBase64(settings.method + ':' + password.join(':'))}@${address}:${this.port}`;
+        let link = `ss://${safeBase64(settings.method + ':' + password.join(':'))}@${address}:${port}`;
         const url = new URL(link);
         for (const [key, value] of params) {
             url.searchParams.set(key, value)
@@ -1457,9 +1445,8 @@ class Inbound extends XrayCommonClass {
         return url.toString();
     }
 
-    genTrojanLink(address = '', remark = '', clientIndex = 0) {
-        let settings = this.settings;
-        const port = this.port;
+    genTrojanLink(address = '', port=this.port, forceTls, remark = '', clientPassword) {
+        const security = forceTls == 'same' ? this.stream.security : forceTls;
         const type = this.stream.network;
         const params = new Map();
         params.set("type", this.stream.network);
@@ -1511,48 +1498,41 @@ class Inbound extends XrayCommonClass {
                 break;
         }
 
-        if (this.tls) {
+        if (security === 'tls') {
             params.set("security", "tls");
-            params.set("fp" , this.stream.tls.settings.fingerprint);
-            params.set("alpn", this.stream.tls.alpn);
-            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.isTls){
+                params.set("fp" , this.stream.tls.settings.fingerprint);
+                params.set("alpn", this.stream.tls.alpn);
+                if(this.stream.tls.settings.allowInsecure){
+                    params.set("allowInsecure", "1");
+                }
+                if (!ObjectUtil.isEmpty(this.stream.tls.server)){
+                    params.set("sni", this.stream.tls.server);
+                }
             }
-            if (this.stream.tls.settings.serverName !== ''){
-                params.set("sni", this.stream.tls.settings.serverName);
-			}
         }
 
-        else if (this.reality) {
+        else if (security === 'reality') {
             params.set("security", "reality");
-            params.set("fp", this.stream.reality.settings.fingerprint);
             params.set("pbk", this.stream.reality.settings.publicKey);
+            params.set("fp", this.stream.reality.settings.fingerprint);
             if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
                 params.set("sni", this.stream.reality.serverNames.split(",")[0]);
             }
             if (this.stream.reality.shortIds.length > 0) {
                 params.set("sid", this.stream.reality.shortIds.split(",")[0]);
             }
-            if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
-                address = this.stream.reality.settings.serverName;
-            }
             if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
                 params.set("spx", this.stream.reality.settings.spiderX);
             }
         }
 
-		else if (this.xtls) {
+		else if (security === 'xtls') {
             params.set("security", "xtls");
             params.set("alpn", this.stream.xtls.alpn);
             if(this.stream.xtls.settings.allowInsecure){
                 params.set("allowInsecure", "1");
             }
-            if (!ObjectUtil.isEmpty(this.stream.xtls.server)) {
-                address = this.stream.xtls.server;
-			}
             if (this.stream.xtls.settings.serverName !== ''){
                 params.set("sni", this.stream.xtls.settings.serverName);
 			}
@@ -1563,7 +1543,7 @@ class Inbound extends XrayCommonClass {
             params.set("security", "none");
         }
 
-        const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}`;
+        const link = `trojan://${clientPassword}@${address}:${port}`;
         const url = new URL(link);
         for (const [key, value] of params) {
             url.searchParams.set(key, value)
@@ -1572,38 +1552,55 @@ class Inbound extends XrayCommonClass {
         return url.toString();
     }
 
-    genLink(address='', remark='', clientIndex=0) {
+    genLink(address='', port=this.port, forceTls='same', remark='', client) {
         switch (this.protocol) {
-            case Protocols.VMESS:  
-                return this.genVmessLink(address, remark, clientIndex);
+            case Protocols.VMESS:
+                return this.genVmessLink(address, port, forceTls, remark, client.id);
             case Protocols.VLESS:
-                return this.genVLESSLink(address, remark, clientIndex);
+                return this.genVLESSLink(address, port, forceTls, remark, client.id, client.flow);
             case Protocols.SHADOWSOCKS: 
-                return this.genSSLink(address, remark, clientIndex);
+                return this.genSSLink(address, port, forceTls, remark, this.isSSMultiUser ? client.password : '');
             case Protocols.TROJAN:
-                return this.genTrojanLink(address, remark, clientIndex);
+                return this.genTrojanLink(address, port, forceTls, remark, client.password);
             default: return '';
         }
     }
 
-    genInboundLinks(address = '', remark = '') {
-        let link = '';
-        switch (this.protocol) {
-            case Protocols.VMESS:
-            case Protocols.VLESS:
-            case Protocols.TROJAN:
-            case Protocols.SHADOWSOCKS:
-                JSON.parse(this.settings).clients.forEach((client,index) => {
-                    if(this.tls && !ObjectUtil.isArrEmpty(this.stream.tls.settings.domains)){
-                        this.stream.tls.settings.domains.forEach((domain) => {
-                            link += this.genLink(domain.domain, [remark, client.email, domain.remark].filter(x => x.length > 0).join('-'), index) + '\r\n';
-                        });
-                    } else {
-                        link += this.genLink(address, [remark, client.email].filter(x => x.length > 0).join('-'), index) + '\r\n';
-                    }
+    genAllLinks(remark='', client){
+        let result = [];
+        let email = client ? client.email : '';
+        let addr = !ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0" ? this.listen : location.hostname;
+        let port = this.port
+        if(ObjectUtil.isArrEmpty(this.stream.externalProxy)){
+            let r = [remark, email].filter(x => x.length > 0).join('-');
+            result.push({
+                remark: r,
+                link: this.genLink(addr, port, 'same', r, client)
+            });
+        } else {
+            this.stream.externalProxy.forEach((ep) => {
+                let r = [remark, email, ep.remark].filter(x => x.length > 0).join('-')
+                result.push({
+                    remark: r,
+                    link: this.genLink(ep.dest, ep.port, ep.forceTls, r, client)
                 });
-                return link;
-            default: return '';
+            });
+        }
+        return result;
+    }
+
+    genInboundLinks(remark = '') {
+        if(this.clients){
+           let links = [];
+           this.clients.forEach((client) => {
+                genAllLinks(remark,client).forEach(l => {
+                    links.push(l.link);
+                })
+            });
+            return links.join('\r\n');
+        } else {
+            if(this.protocol == Protocols.SHADOWSOCKS && !this.isSSMultiUser) return this.genSSLink(this.listen, this.port, remark);
+            return '';
         }
     }
 

+ 6 - 20
web/html/common/qrcode_modal.html

@@ -22,39 +22,25 @@
 
     const qrModal = {
         title: '',
-        clientIndex: 0,
-        inbound: new Inbound(),
         dbInbound: new DBInbound(),
         client: null,
         qrcodes: [],
         clipboard: null,
         visible: false,
         subId: '',
-        show: function (title = '', dbInbound = new DBInbound(), clientIndex = 0) {
+        show: function (title = '', dbInbound, client) {
             this.title = title;
-            this.clientIndex = clientIndex;
             this.dbInbound = dbInbound;
             this.inbound = dbInbound.toInbound();
-            settings = JSON.parse(this.inbound.settings);
-            this.client = settings.clients[clientIndex];
-            remark = [this.dbInbound.remark, ( this.client ? this.client.email : '')].filter(Boolean).join('-');
-            address = this.dbInbound.address;
+            this.client = client;
             this.subId = '';
             this.qrcodes = [];
-            if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
-                this.inbound.stream.tls.settings.domains.forEach((domain) => {
-                    remarkText = [remark, domain.remark].filter(Boolean).join('-');
-                    this.qrcodes.push({
-                        remark: remarkText,
-                        link: this.inbound.genLink(domain.domain, remarkText, clientIndex)
-                    });
-                });
-            } else {
+            this.inbound.genAllLinks(this.dbInbound.remark, client).forEach(l => {
                 this.qrcodes.push({
-                    remark: remark,
-                    link: this.inbound.genLink(address, remark, clientIndex)
+                    remark: l.remark,
+                    link: l.link
                 });
-            }
+            });
             this.visible = true;
         },
         close: function () {

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

@@ -38,7 +38,7 @@
             this.isEdit = isEdit;
             this.dbInbound = new DBInbound(dbInbound);
             this.inbound = dbInbound.toInbound();
-            this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
+            this.clients = this.inbound.clients;
             this.index = index === null ? this.clients.length : index;
             this.delayedStart = false;
             if (isEdit) {
@@ -51,16 +51,7 @@
             }
             this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
             this.confirm = confirm;
-        },
-        getClients(protocol, clientSettings) {
-            switch (protocol) {
-                case Protocols.VMESS: return clientSettings.vmesses;
-                case Protocols.VLESS: return clientSettings.vlesses;
-                case Protocols.TROJAN: return clientSettings.trojans;
-                case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
-                default: return null;
-            }
-        },
+        },  
         getClientId(protocol, client) {
             switch (protocol) {
                 case Protocols.TROJAN: return client.password;

+ 1 - 0
web/html/xui/form/inbound.html

@@ -96,6 +96,7 @@
 <!-- stream settings -->
 <template v-if="inbound.canEnableStream()">
     {{template "form/streamSettings"}}
+    {{template "form/externalProxy" }}
 </template>
 
 <!-- tls settings -->

+ 32 - 0
web/html/xui/form/stream/external_proxy.html

@@ -0,0 +1,32 @@
+{{define "form/externalProxy"}}
+<a-form layout="inline">
+    <a-divider style="margin:0;"></a-divider>
+    <a-form-item label="External Proxy">
+    <a-switch v-model="externalProxy"></a-switch>
+    <a-button v-if="externalProxy" type="primary" style="margin-left: 10px" size="small" @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})">+</a-button>
+    </a-form-item>
+    <table width="100%" class="ant-table-tbody" v-if="externalProxy" style="margin-bottom:5px">
+        <tr style="line-height: 40px;">
+            <td width="100%">
+                <a-input-group style="margin: 0 5px;" compact v-for="(row, index) in inbound.stream.externalProxy">
+                    <template>
+                        <a-tooltip title="Force TLS">
+                            <a-select v-model="row.forceTls" style="width:20%; margin: 0px" :dropdown-class-name="themeSwitcher.currentTheme">
+                                <a-select-option value="same">{{ i18n "pages.inbounds.same" }}</a-select-option>
+                                <a-select-option value="none">{{ i18n "none" }}</a-select-option>
+                                <a-select-option value="tls">TLS</a-select-option>
+                            </a-select>
+                        </a-tooltip>
+                    </template>
+                    <a-input style="width: 35%" 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-tooltip>
+                    <a-input style="width: 20%" 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>
+            </td>
+        </tr>
+    </table>
+</a-form>
+{{end}}

+ 2 - 28
web/html/xui/form/tls_settings.html

@@ -24,26 +24,6 @@
 
 <!-- tls settings -->
 <a-form v-if="inbound.tls" layout="inline">
-    <a-form-item label='Multi Domain'>
-        <a-switch v-model="multiDomain"></a-switch>
-    </a-form-item>
-    <a-form-item v-if="multiDomain">
-        <a-row>
-            <span>Domains:</span>
-            <a-button v-if="multiDomain" type="primary" size="small" @click="inbound.stream.tls.settings.domains.push({remark: '', domain: ''})" style="margin-left: 10px">+</a-button>
-        </a-row>
-        <a-input-group v-for="(row, index) in inbound.stream.tls.settings.domains">
-            <a-input style="width: 40%" v-model.trim="row.remark" addon-before='{{ i18n "remark" }}'></a-input>
-            <a-input style="width: 60%" v-model.trim="row.domain" addon-before='{{ i18n "host" }}'>
-                <template slot="addonAfter">
-                    <a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tls.settings.domains.splice(index, 1)">-</a-button>
-                </template>
-            </a-input>
-        </a-input-group>
-    </a-form-item>
-    <a-form-item v-else 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" :dropdown-class-name="themeSwitcher.currentTheme">
             <a-select-option value="">auto</a-select-option>
@@ -61,7 +41,7 @@
         </a-input-group>
     </a-form-item>
     <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-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
     </a-form-item>
     <a-form-item label="uTLS">
         <a-select v-model="inbound.stream.tls.settings.fingerprint"
@@ -122,11 +102,8 @@
 
 <!-- xtls settings -->
 <a-form v-else-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="SNI" placeholder="Server Name Indication">
-        <a-input v-model.trim="inbound.stream.xtls.settings.serverName" style="width: 250px"></a-input>
+        <a-input v-model.trim="inbound.stream.xtls.server" style="width: 250px"></a-input>
     </a-form-item>
     <a-form-item label="Alpn">
         <a-checkbox-group v-model="inbound.stream.xtls.alpn" style="width:200px">
@@ -179,9 +156,6 @@
                     style="width: 135px" :dropdown-class-name="themeSwitcher.currentTheme">
             <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: 300px"></a-input>

+ 4 - 21
web/html/xui/inbound_info_modal.html

@@ -265,27 +265,10 @@
             this.index = index;
             this.inbound = dbInbound.toInbound();
             this.dbInbound = new DBInbound(dbInbound);
-            this.settings = JSON.parse(this.inbound.settings);
-            this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
-            this.isExpired = this.inbound.isExpiry(index);
-            this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
-            remark = [this.dbInbound.remark, ( this.clientSettings ? this.clientSettings.email : '')].filter(Boolean).join('-');
-            address = this.dbInbound.address;
-            this.links = [];
-            if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
-                this.inbound.stream.tls.settings.domains.forEach((domain) => {
-                    remarkText = [remark, domain.remark].filter(Boolean).join('-');
-                    this.links.push({
-                        remark: remarkText,
-                        link: this.inbound.genLink(domain.domain, remarkText, index)
-                    });
-                });
-            } else {
-                this.links.push({
-                    remark: remark,
-                    link: this.inbound.genLink(address, remark, index)
-                });
-            }
+            this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
+            this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index): this.dbInbound.isExpiry;
+            this.clientStats = this.inbound.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
+            this.links = this.inbound.genAllLinks(this.dbInbound.remark, this.clientSettings);
             if (this.clientSettings) {
                 if (this.clientSettings.subId) {
                     this.subLink = this.genSubLink(this.clientSettings.subId);

+ 11 - 17
web/html/xui/inbound_modal.html

@@ -43,15 +43,6 @@
         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;
-                case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
-                default: return null;
-            }
-        },
     };
 
     new Vue({
@@ -70,7 +61,7 @@
                 return inModal.isEdit;
             },
             get client() {
-                return inModal.getClients(this.inbound.protocol, this.inbound.settings)[0];
+                return inModal.inbound.clients[0];
             },
             get delayedExpireDays() {
                 return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
@@ -78,16 +69,19 @@
             set delayedExpireDays(days) {
                 this.client.expiryTime = -86400000 * days;
             },
-            get multiDomain() {
-                return this.inbound.stream.tls.settings.domains.length > 0;
+            get externalProxy() {
+                return this.inbound.stream.externalProxy.length > 0;
             },
-            set multiDomain(value) {
+            set externalProxy(value) {
                 if (value) {
-                    inModal.inbound.stream.tls.server = "";
-                    inModal.inbound.stream.tls.settings.domains = [{ remark: "", domain: window.location.hostname }];
+                    inModal.inbound.stream.externalProxy = [{
+                        forceTls: "same",
+                        dest: window.location.hostname,
+                        port: inModal.inbound.port,
+                        remark: ""
+                    }];
                 } else {
-                    inModal.inbound.stream.tls.server = "";
-                    inModal.inbound.stream.tls.settings.domains = [];
+                    inModal.inbound.stream.externalProxy = [];
                 }
             }
         },

+ 15 - 31
web/html/xui/inbounds.html

@@ -618,7 +618,7 @@
             },
             getClientCounts(dbInbound, inbound) {
                 let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [], online = [];
-                clients = this.getClients(dbInbound.protocol, inbound.settings);
+                clients = inbound.clients;
                 clientStats = dbInbound.clientStats
                 now = new Date().getTime()
                 if (clients) {
@@ -968,15 +968,6 @@
                     this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`);
                 }
             },
-            getClients(protocol, clientSettings) {
-                switch (protocol) {
-                    case Protocols.VMESS: return clientSettings.vmesses;
-                    case Protocols.VLESS: return clientSettings.vlesses;
-                    case Protocols.TROJAN: return clientSettings.trojans;
-                    case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
-                    default: return null;
-                }
-            },
             getClientId(protocol, client) {
                 switch (protocol) {
                     case Protocols.TROJAN: return client.password;
@@ -996,8 +987,9 @@
                         newDbInbound.listen = rootInbound.listen;
                         newDbInbound.port = rootInbound.port;
                         newInbound = newDbInbound.toInbound();
-                        newInbound.stream.security = 'tls';
+                        newInbound.stream.security = rootInbound.stream.security;
                         newInbound.stream.tls = rootInbound.stream.tls;
+                        newInbound.stream.externalProxy = rootInbound.stream.externalProxy;
                         newDbInbound.streamSettings = newInbound.stream.toString();
                     }
                 }
@@ -1005,17 +997,17 @@
             },
             showQrcode(dbInboundId, client) {
                 dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
-                inbound = dbInbound.toInbound();
-                clients = this.getClients(dbInbound.protocol, inbound.settings);
-                index = this.findIndexOfClient(dbInbound.protocol, clients, client);
                 newDbInbound = this.checkFallback(dbInbound);
-                qrModal.show('{{ i18n "qrCode"}}', newDbInbound, index);
+                qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client);
             },
             showInfo(dbInboundId, client) {
                 dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
-                inbound = dbInbound.toInbound();
-                clients = this.getClients(dbInbound.protocol, inbound.settings);
-                index = this.findIndexOfClient(dbInbound.protocol, clients, client);
+                index=0;
+                if (dbInbound.isMultiUser()){
+                    inbound = dbInbound.toInbound();
+                    clients = inbound.clients;
+                    index = this.findIndexOfClient(dbInbound.protocol, clients, client);
+                }
                 newDbInbound = this.checkFallback(dbInbound);
                 infoModal.show(newDbInbound, index);
             },
@@ -1027,7 +1019,7 @@
                 this.loading()
                 dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
                 inbound = dbInbound.toInbound();
-                clients = this.getClients(dbInbound.protocol, inbound.settings);
+                clients = inbound.clients;
                 index = this.findIndexOfClient(dbInbound.protocol, clients, client);
                 clients[index].enable = !clients[index].enable;
                 clientId = this.getClientId(dbInbound.protocol, clients[index]);
@@ -1041,15 +1033,7 @@
                 }
             },
             getInboundClients(dbInbound) {
-                if (dbInbound.protocol == Protocols.VLESS) {
-                    return dbInbound.toInbound().settings.vlesses;
-                } else if (dbInbound.protocol == Protocols.VMESS) {
-                    return dbInbound.toInbound().settings.vmesses;
-                } else if (dbInbound.protocol == Protocols.TROJAN) {
-                    return dbInbound.toInbound().settings.trojans;
-                } else if (dbInbound.protocol == Protocols.SHADOWSOCKS) {
-                    return dbInbound.toInbound().settings.shadowsockses;
-                }
+                return dbInbound.toInbound().clients;
             },
             resetClientTraffic(client, dbInboundId, confirmation = true) {
                 if (confirmation){
@@ -1179,11 +1163,11 @@
                 txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks, newDbInbound.remark);
             },
             exportAllLinks() {
-                let copyText = '';
+                let copyText = [];
                 for (const dbInbound of this.dbInbounds) {
-                    copyText += dbInbound.genInboundLinks;
+                    copyText.push(dbInbound.genInboundLinks);
                 }
-                txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText, 'All-Inbounds');
+                txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText.join('\r\n'), 'All-Inbounds');
             },
             async startDataRefreshLoop() {
                 while (this.isRefreshEnabled) {

+ 40 - 0
web/service/inbound.go

@@ -1795,6 +1795,46 @@ func (s *InboundService) MigrationRequirements() {
 
 	// Remove orphaned traffics
 	tx.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
+
+	// Migrate old MultiDomain to External Proxy
+	var externalProxy []struct {
+		Id             int
+		Port           int
+		StreamSettings []byte
+	}
+	err = tx.Raw(`select id, port, stream_settings
+	from inbounds
+	WHERE protocol in ('vmess','vless','trojan')
+	  AND json_extract(stream_settings, '$.security') = 'tls'
+	  AND json_extract(stream_settings, '$.tlsSettings.settings.domains') IS NOT NULL`).Scan(&externalProxy).Error
+	if err != nil || len(externalProxy) == 0 {
+		return
+	}
+
+	for _, ep := range externalProxy {
+		var reverses interface{}
+		var stream map[string]interface{}
+		json.Unmarshal(ep.StreamSettings, &stream)
+		if tlsSettings, ok := stream["tlsSettings"].(map[string]interface{}); ok {
+			if settings, ok := tlsSettings["settings"].(map[string]interface{}); ok {
+				if domains, ok := settings["domains"].([]interface{}); ok {
+					for _, domain := range domains {
+						if domainMap, ok := domain.(map[string]interface{}); ok {
+							domainMap["forceTls"] = "same"
+							domainMap["port"] = ep.Port
+							domainMap["dest"] = domainMap["domain"].(string)
+							delete(domainMap, "domain")
+						}
+					}
+				}
+				reverses = settings["domains"]
+				delete(settings, "domains")
+			}
+		}
+		stream["externalProxy"] = reverses
+		newStream, _ := json.MarshalIndent(stream, " ", "  ")
+		tx.Model(model.Inbound{}).Where("id = ?", ep.Id).Update("stream_settings", newStream)
+	}
 }
 
 func (s *InboundService) MigrateDB() {

+ 17 - 12
web/service/xray.go

@@ -135,19 +135,24 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
 			inbound.Settings = string(modifiedSettings)
 		}
 
-		// Unmarshal stream JSON
-		var stream map[string]interface{}
-		json.Unmarshal([]byte(inbound.StreamSettings), &stream)
-
-		// Remove the "settings" field under "tlsSettings" and "realitySettings"
-		tlsSettings, ok1 := stream["tlsSettings"].(map[string]interface{})
-		realitySettings, ok2 := stream["realitySettings"].(map[string]interface{})
-		if ok1 || ok2 {
-			if ok1 {
-				delete(tlsSettings, "settings")
-			} else if ok2 {
-				delete(realitySettings, "settings")
+		if len(inbound.StreamSettings) > 0 {
+			// Unmarshal stream JSON
+			var stream map[string]interface{}
+			json.Unmarshal([]byte(inbound.StreamSettings), &stream)
+
+			// Remove the "settings" field under "tlsSettings" and "realitySettings"
+			tlsSettings, ok1 := stream["tlsSettings"].(map[string]interface{})
+			realitySettings, ok2 := stream["realitySettings"].(map[string]interface{})
+			if ok1 || ok2 {
+				if ok1 {
+					delete(tlsSettings, "settings")
+				} else if ok2 {
+					delete(realitySettings, "settings")
+				}
 			}
+
+			delete(stream, "externalProxy")
+
 			newStream, err := json.MarshalIndent(stream, "", "  ")
 			if err != nil {
 				return nil, err

+ 1 - 0
web/translation/translate.en_US.toml

@@ -176,6 +176,7 @@
 "telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot or use '/id' command in bot )"
 "subscriptionDesc" = "you can find your sub link on Details, also you can use the same name for several configurations"
 "info" = "Info"
+"same" = "Same"
 
 [pages.client]
 "add" = "Add Client"

+ 1 - 0
web/translation/translate.es_ES.toml

@@ -176,6 +176,7 @@
 "telegramDesc" = "Utiliza el ID de Telegram sin @ o los IDs de chat (puedes obtenerlo aquí @userinfobot o usando el comando '/id' en el bot)."
 "subscriptionDesc" = "Puedes encontrar tu enlace de suscripción en Detalles, también puedes usar el mismo nombre para varias configuraciones."
 "info" = "Info"
+"same" = "misma"
 
 [pages.client]
 "add" = "Agregar Cliente"

+ 1 - 0
web/translation/translate.fa_IR.toml

@@ -176,6 +176,7 @@
 "telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)"
 "subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
 "info" = "اطلاعات"
+"same" = "همسان"
 
 [pages.client]
 "add" = "کاربر جدید"

+ 1 - 0
web/translation/translate.ru_RU.toml

@@ -176,6 +176,7 @@
 "telegramDesc" = "Используйте идентификатор Telegram без символа @ или идентификатора чата (можно получить его здесь @userinfobot или использовать команду '/id' в боте)"
 "subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе 'Подробнее', также вы можете использовать одно и то же имя для нескольких конфигураций"
 "info" = "Информация"
+"same" = "Тот же"
 
 [pages.client]
 "add" = "Добавить пользователя"

+ 1 - 0
web/translation/translate.vi_VN.toml

@@ -176,6 +176,7 @@
 "telegramDesc" = "Sử dụng Telegram ID mà không cần ký hiệu @ hoặc chat IDs (bạn có thể nhận được nó ở đây @userinfobot hoặc sử dụng lệnh '/id' trong bot)"
 "subscriptionDesc" = "Bạn có thể tìm liên kết đăng ký của mình trong Chi tiết, cũng như bạn có thể sử dụng cùng tên cho nhiều cấu hình khác nhau"
 "info" = "Thông tin"
+"same" = "Giống nhau"
 
 [pages.client]
 "add" = "Thêm Client"

+ 1 - 0
web/translation/translate.zh_Hans.toml

@@ -176,6 +176,7 @@
 "telegramDesc" = "使用 Telegram ID,不包含 @ 符号或聊天 ID(可以在 @userinfobot 处获取,或在机器人中使用'/id'命令)"
 "subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称"
 "info" = "信息"
+"same" = "相同"
 
 [pages.client]
 "add" = "添加客户端"