diff --git a/sub/subService.go b/sub/subService.go index 272bf9d5..67b931ce 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -3,8 +3,10 @@ package sub import ( "encoding/base64" "fmt" + "maps" "net" "net/url" + "slices" "strings" "time" @@ -179,186 +181,54 @@ func (s *SubService) getLink(inbound *model.Inbound, email string) string { return "" } +// Protocol link generators are intentionally ordered as: +// vmess -> vless -> trojan -> shadowsocks -> hysteria. func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string { if inbound.Protocol != model.VMESS { return "" } - var address string - if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { - address = s.address - } else { - address = inbound.Listen - } + address := s.resolveInboundAddress(inbound) obj := map[string]any{ "v": "2", "add": address, "port": inbound.Port, "type": "none", } - var stream map[string]any - json.Unmarshal([]byte(inbound.StreamSettings), &stream) + stream := unmarshalStreamSettings(inbound.StreamSettings) network, _ := stream["network"].(string) - obj["net"] = network - switch network { - case "tcp": - tcp, _ := stream["tcpSettings"].(map[string]any) - header, _ := tcp["header"].(map[string]any) - typeStr, _ := header["type"].(string) - obj["type"] = typeStr - if typeStr == "http" { - request := header["request"].(map[string]any) - requestPath, _ := request["path"].([]any) - obj["path"] = requestPath[0].(string) - headers, _ := request["headers"].(map[string]any) - obj["host"] = searchHost(headers) - } - case "kcp": - kcp, _ := stream["kcpSettings"].(map[string]any) - header, _ := kcp["header"].(map[string]any) - obj["type"], _ = header["type"].(string) - obj["path"], _ = kcp["seed"].(string) - case "ws": - ws, _ := stream["wsSettings"].(map[string]any) - obj["path"] = ws["path"].(string) - if host, ok := ws["host"].(string); ok && len(host) > 0 { - obj["host"] = host - } else { - headers, _ := ws["headers"].(map[string]any) - obj["host"] = searchHost(headers) - } - case "grpc": - grpc, _ := stream["grpcSettings"].(map[string]any) - obj["path"] = grpc["serviceName"].(string) - obj["authority"] = grpc["authority"].(string) - if grpc["multiMode"].(bool) { - obj["type"] = "multi" - } - case "httpupgrade": - httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any) - obj["path"] = httpupgrade["path"].(string) - if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 { - obj["host"] = host - } else { - headers, _ := httpupgrade["headers"].(map[string]any) - obj["host"] = searchHost(headers) - } - case "xhttp": - xhttp, _ := stream["xhttpSettings"].(map[string]any) - obj["path"] = xhttp["path"].(string) - if host, ok := xhttp["host"].(string); ok && len(host) > 0 { - obj["host"] = host - } else { - headers, _ := xhttp["headers"].(map[string]any) - obj["host"] = searchHost(headers) - } - obj["mode"], _ = xhttp["mode"].(string) - // VMess base64 JSON supports arbitrary keys; copy the padding - // settings through so clients can match the server's xhttp - // xPaddingBytes range and, when the admin opted into obfs - // mode, the custom key / header / placement / method. - if xpb, ok := xhttp["xPaddingBytes"].(string); ok && len(xpb) > 0 { - obj["x_padding_bytes"] = xpb - } - if obfs, ok := xhttp["xPaddingObfsMode"].(bool); ok && obfs { - obj["xPaddingObfsMode"] = true - for _, field := range []string{"xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"} { - if v, ok := xhttp[field].(string); ok && len(v) > 0 { - obj[field] = v - } - } - } + applyVmessNetworkParams(stream, network, obj) + if finalmask, ok := stream["finalmask"].(map[string]any); ok { + applyFinalMaskObj(finalmask, obj) } security, _ := stream["security"].(string) obj["tls"] = security if security == "tls" { - tlsSetting, _ := stream["tlsSettings"].(map[string]any) - alpns, _ := tlsSetting["alpn"].([]any) - if len(alpns) > 0 { - var alpn []string - for _, a := range alpns { - alpn = append(alpn, a.(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 fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { - obj["fp"], _ = fpValue.(string) - } - } + applyVmessTLSParams(stream, obj) } clients, _ := s.inboundService.GetClients(inbound) - clientIndex := -1 - for i, client := range clients { - if client.Email == email { - clientIndex = i - break - } - } + clientIndex := findClientIndex(clients, email) obj["id"] = clients[clientIndex].ID obj["scy"] = clients[clientIndex].Security externalProxies, _ := stream["externalProxy"].([]any) if len(externalProxies) > 0 { - links := "" - for index, externalProxy := range externalProxies { - ep, _ := externalProxy.(map[string]any) - newSecurity, _ := ep["forceTls"].(string) - newObj := map[string]any{} - for key, value := range obj { - if !(newSecurity == "none" && (key == "alpn" || key == "sni" || key == "fp")) { - 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(newObj, "", " ") - links += "vmess://" + base64.StdEncoding.EncodeToString(jsonStr) - } - return links + return s.buildVmessExternalProxyLinks(externalProxies, obj, inbound, email) } obj["ps"] = s.genRemark(inbound, email, "") - - jsonStr, _ := json.MarshalIndent(obj, "", " ") - return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr) + return buildVmessLink(obj) } func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { - var address string - if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { - address = s.address - } else { - address = inbound.Listen - } - if inbound.Protocol != model.VLESS { return "" } - var stream map[string]any - json.Unmarshal([]byte(inbound.StreamSettings), &stream) + address := s.resolveInboundAddress(inbound) + stream := unmarshalStreamSettings(inbound.StreamSettings) clients, _ := s.inboundService.GetClients(inbound) - clientIndex := -1 - for i, client := range clients { - if client.Email == email { - clientIndex = i - break - } - } + clientIndex := findClientIndex(clients, email) uuid := clients[clientIndex].ID port := inbound.Port streamNetwork := stream["network"].(string) @@ -372,481 +242,122 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { params["encryption"] = encryption } - switch streamNetwork { - case "tcp": - tcp, _ := stream["tcpSettings"].(map[string]any) - header, _ := tcp["header"].(map[string]any) - typeStr, _ := header["type"].(string) - if typeStr == "http" { - request := header["request"].(map[string]any) - requestPath, _ := request["path"].([]any) - params["path"] = requestPath[0].(string) - headers, _ := request["headers"].(map[string]any) - params["host"] = searchHost(headers) - params["headerType"] = "http" - } - case "kcp": - kcp, _ := stream["kcpSettings"].(map[string]any) - header, _ := kcp["header"].(map[string]any) - params["headerType"] = header["type"].(string) - params["seed"] = kcp["seed"].(string) - case "ws": - ws, _ := stream["wsSettings"].(map[string]any) - params["path"] = ws["path"].(string) - if host, ok := ws["host"].(string); ok && len(host) > 0 { - params["host"] = host - } else { - headers, _ := ws["headers"].(map[string]any) - params["host"] = searchHost(headers) - } - case "grpc": - grpc, _ := stream["grpcSettings"].(map[string]any) - params["serviceName"] = grpc["serviceName"].(string) - params["authority"], _ = grpc["authority"].(string) - if grpc["multiMode"].(bool) { - params["mode"] = "multi" - } - case "httpupgrade": - httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any) - params["path"] = httpupgrade["path"].(string) - if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 { - params["host"] = host - } else { - headers, _ := httpupgrade["headers"].(map[string]any) - params["host"] = searchHost(headers) - } - case "xhttp": - xhttp, _ := stream["xhttpSettings"].(map[string]any) - params["path"] = xhttp["path"].(string) - if host, ok := xhttp["host"].(string); ok && len(host) > 0 { - params["host"] = host - } else { - headers, _ := xhttp["headers"].(map[string]any) - params["host"] = searchHost(headers) - } - params["mode"], _ = xhttp["mode"].(string) - applyXhttpPaddingParams(xhttp, params) + applyShareNetworkParams(stream, streamNetwork, params) + if finalmask, ok := stream["finalmask"].(map[string]any); ok { + applyFinalMaskParams(finalmask, params) } security, _ := stream["security"].(string) - if security == "tls" { - params["security"] = "tls" - tlsSetting, _ := stream["tlsSettings"].(map[string]any) - alpns, _ := tlsSetting["alpn"].([]any) - 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) - } - } - + switch security { + case "tls": + applyShareTLSParams(stream, params) if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { params["flow"] = clients[clientIndex].Flow } - } - - if security == "reality" { - params["security"] = "reality" - realitySetting, _ := stream["realitySettings"].(map[string]any) - realitySettings, _ := searchKey(realitySetting, "settings") - if realitySetting != nil { - if sniValue, ok := searchKey(realitySetting, "serverNames"); ok { - sNames, _ := sniValue.([]any) - params["sni"] = sNames[random.Num(len(sNames))].(string) - } - if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok { - params["pbk"], _ = pbkValue.(string) - } - if sidValue, ok := searchKey(realitySetting, "shortIds"); ok { - shortIds, _ := sidValue.([]any) - params["sid"] = shortIds[random.Num(len(shortIds))].(string) - } - if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok { - if fp, ok := fpValue.(string); ok && len(fp) > 0 { - params["fp"] = fp - } - } - if pqvValue, ok := searchKey(realitySettings, "mldsa65Verify"); ok { - if pqv, ok := pqvValue.(string); ok && len(pqv) > 0 { - params["pqv"] = pqv - } - } - params["spx"] = "/" + random.Seq(15) - } - + case "reality": + applyShareRealityParams(stream, params) if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { params["flow"] = clients[clientIndex].Flow } - } - - if security != "tls" && security != "reality" { + default: params["security"] = "none" } externalProxies, _ := stream["externalProxy"].([]any) if len(externalProxies) > 0 { - links := make([]string, 0, len(externalProxies)) - for _, externalProxy := range externalProxies { - ep, _ := externalProxy.(map[string]any) - 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() - - for k, v := range params { - if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp")) { - 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)) - - links = append(links, url.String()) - } - return strings.Join(links, "\n") + return s.buildExternalProxyURLLinks( + externalProxies, + params, + security, + func(dest string, port int) string { + return fmt.Sprintf("vless://%s@%s:%d", uuid, dest, port) + }, + func(ep map[string]any) string { + return s.genRemark(inbound, email, ep["remark"].(string)) + }, + ) } 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() + return buildLinkWithParams(link, params, s.genRemark(inbound, email, "")) } func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string { - var address string - if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { - address = s.address - } else { - address = inbound.Listen - } if inbound.Protocol != model.Trojan { return "" } - var stream map[string]any - json.Unmarshal([]byte(inbound.StreamSettings), &stream) + address := s.resolveInboundAddress(inbound) + stream := unmarshalStreamSettings(inbound.StreamSettings) clients, _ := s.inboundService.GetClients(inbound) - clientIndex := -1 - for i, client := range clients { - if client.Email == email { - clientIndex = i - break - } - } + clientIndex := findClientIndex(clients, email) password := clients[clientIndex].Password port := inbound.Port streamNetwork := stream["network"].(string) params := make(map[string]string) params["type"] = streamNetwork - switch streamNetwork { - case "tcp": - tcp, _ := stream["tcpSettings"].(map[string]any) - header, _ := tcp["header"].(map[string]any) - typeStr, _ := header["type"].(string) - if typeStr == "http" { - request := header["request"].(map[string]any) - requestPath, _ := request["path"].([]any) - params["path"] = requestPath[0].(string) - headers, _ := request["headers"].(map[string]any) - params["host"] = searchHost(headers) - params["headerType"] = "http" - } - case "kcp": - kcp, _ := stream["kcpSettings"].(map[string]any) - header, _ := kcp["header"].(map[string]any) - params["headerType"] = header["type"].(string) - params["seed"] = kcp["seed"].(string) - case "ws": - ws, _ := stream["wsSettings"].(map[string]any) - params["path"] = ws["path"].(string) - if host, ok := ws["host"].(string); ok && len(host) > 0 { - params["host"] = host - } else { - headers, _ := ws["headers"].(map[string]any) - params["host"] = searchHost(headers) - } - case "grpc": - grpc, _ := stream["grpcSettings"].(map[string]any) - params["serviceName"] = grpc["serviceName"].(string) - params["authority"], _ = grpc["authority"].(string) - if grpc["multiMode"].(bool) { - params["mode"] = "multi" - } - case "httpupgrade": - httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any) - params["path"] = httpupgrade["path"].(string) - if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 { - params["host"] = host - } else { - headers, _ := httpupgrade["headers"].(map[string]any) - params["host"] = searchHost(headers) - } - case "xhttp": - xhttp, _ := stream["xhttpSettings"].(map[string]any) - params["path"] = xhttp["path"].(string) - if host, ok := xhttp["host"].(string); ok && len(host) > 0 { - params["host"] = host - } else { - headers, _ := xhttp["headers"].(map[string]any) - params["host"] = searchHost(headers) - } - params["mode"], _ = xhttp["mode"].(string) - applyXhttpPaddingParams(xhttp, params) + applyShareNetworkParams(stream, streamNetwork, params) + if finalmask, ok := stream["finalmask"].(map[string]any); ok { + applyFinalMaskParams(finalmask, params) } security, _ := stream["security"].(string) - if security == "tls" { - params["security"] = "tls" - tlsSetting, _ := stream["tlsSettings"].(map[string]any) - alpns, _ := tlsSetting["alpn"].([]any) - 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 security == "reality" { - params["security"] = "reality" - realitySetting, _ := stream["realitySettings"].(map[string]any) - realitySettings, _ := searchKey(realitySetting, "settings") - if realitySetting != nil { - if sniValue, ok := searchKey(realitySetting, "serverNames"); ok { - sNames, _ := sniValue.([]any) - params["sni"] = sNames[random.Num(len(sNames))].(string) - } - if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok { - params["pbk"], _ = pbkValue.(string) - } - if sidValue, ok := searchKey(realitySetting, "shortIds"); ok { - shortIds, _ := sidValue.([]any) - params["sid"] = shortIds[random.Num(len(shortIds))].(string) - } - if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok { - if fp, ok := fpValue.(string); ok && len(fp) > 0 { - params["fp"] = fp - } - } - if pqvValue, ok := searchKey(realitySettings, "mldsa65Verify"); ok { - if pqv, ok := pqvValue.(string); ok && len(pqv) > 0 { - params["pqv"] = pqv - } - } - params["spx"] = "/" + random.Seq(15) - } - + switch security { + case "tls": + applyShareTLSParams(stream, params) + case "reality": + applyShareRealityParams(stream, params) if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { params["flow"] = clients[clientIndex].Flow } - } - - if security != "tls" && security != "reality" { + default: params["security"] = "none" } externalProxies, _ := stream["externalProxy"].([]any) if len(externalProxies) > 0 { - links := "" - for index, externalProxy := range externalProxies { - ep, _ := externalProxy.(map[string]any) - 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 { - if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp")) { - 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 + return s.buildExternalProxyURLLinks( + externalProxies, + params, + security, + func(dest string, port int) string { + return fmt.Sprintf("trojan://%s@%s:%d", password, dest, port) + }, + func(ep map[string]any) string { + return s.genRemark(inbound, email, ep["remark"].(string)) + }, + ) } 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() + return buildLinkWithParams(link, params, s.genRemark(inbound, email, "")) } func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string { - var address string - if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { - address = s.address - } else { - address = inbound.Listen - } if inbound.Protocol != model.Shadowsocks { return "" } - var stream map[string]any - json.Unmarshal([]byte(inbound.StreamSettings), &stream) + address := s.resolveInboundAddress(inbound) + stream := unmarshalStreamSettings(inbound.StreamSettings) clients, _ := s.inboundService.GetClients(inbound) var settings map[string]any json.Unmarshal([]byte(inbound.Settings), &settings) inboundPassword := settings["password"].(string) method := settings["method"].(string) - clientIndex := -1 - for i, client := range clients { - if client.Email == email { - clientIndex = i - break - } - } + clientIndex := findClientIndex(clients, email) streamNetwork := stream["network"].(string) params := make(map[string]string) params["type"] = streamNetwork - switch streamNetwork { - case "tcp": - tcp, _ := stream["tcpSettings"].(map[string]any) - header, _ := tcp["header"].(map[string]any) - typeStr, _ := header["type"].(string) - if typeStr == "http" { - request := header["request"].(map[string]any) - requestPath, _ := request["path"].([]any) - params["path"] = requestPath[0].(string) - headers, _ := request["headers"].(map[string]any) - params["host"] = searchHost(headers) - params["headerType"] = "http" - } - case "kcp": - kcp, _ := stream["kcpSettings"].(map[string]any) - header, _ := kcp["header"].(map[string]any) - params["headerType"] = header["type"].(string) - params["seed"] = kcp["seed"].(string) - case "ws": - ws, _ := stream["wsSettings"].(map[string]any) - params["path"] = ws["path"].(string) - if host, ok := ws["host"].(string); ok && len(host) > 0 { - params["host"] = host - } else { - headers, _ := ws["headers"].(map[string]any) - params["host"] = searchHost(headers) - } - case "grpc": - grpc, _ := stream["grpcSettings"].(map[string]any) - params["serviceName"] = grpc["serviceName"].(string) - params["authority"], _ = grpc["authority"].(string) - if grpc["multiMode"].(bool) { - params["mode"] = "multi" - } - case "httpupgrade": - httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any) - params["path"] = httpupgrade["path"].(string) - if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 { - params["host"] = host - } else { - headers, _ := httpupgrade["headers"].(map[string]any) - params["host"] = searchHost(headers) - } - case "xhttp": - xhttp, _ := stream["xhttpSettings"].(map[string]any) - params["path"] = xhttp["path"].(string) - if host, ok := xhttp["host"].(string); ok && len(host) > 0 { - params["host"] = host - } else { - headers, _ := xhttp["headers"].(map[string]any) - params["host"] = searchHost(headers) - } - params["mode"], _ = xhttp["mode"].(string) - applyXhttpPaddingParams(xhttp, params) + applyShareNetworkParams(stream, streamNetwork, params) + if finalmask, ok := stream["finalmask"].(map[string]any); ok { + applyFinalMaskParams(finalmask, params) } security, _ := stream["security"].(string) if security == "tls" { - params["security"] = "tls" - tlsSetting, _ := stream["tlsSettings"].(map[string]any) - alpns, _ := tlsSetting["alpn"].([]any) - 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) - } - } + applyShareTLSParams(stream, params) } encPart := fmt.Sprintf("%s:%s", method, clients[clientIndex].Password) @@ -857,61 +368,30 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st externalProxies, _ := stream["externalProxy"].([]any) if len(externalProxies) > 0 { - links := "" - for index, externalProxy := range externalProxies { - ep, _ := externalProxy.(map[string]any) - 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")) { - 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 + proxyParams := cloneStringMap(params) + proxyParams["security"] = security + return s.buildExternalProxyURLLinks( + externalProxies, + proxyParams, + security, + func(dest string, port int) string { + return fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), dest, port) + }, + func(ep map[string]any) string { + return s.genRemark(inbound, email, ep["remark"].(string)) + }, + ) } link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.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() + return buildLinkWithParams(link, params, s.genRemark(inbound, email, "")) } func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) string { if !model.IsHysteria(inbound.Protocol) { return "" } - var stream map[string]interface{} + var stream map[string]any json.Unmarshal([]byte(inbound.StreamSettings), &stream) clients, _ := s.inboundService.GetClients(inbound) clientIndex := -1 @@ -925,8 +405,8 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin params := make(map[string]string) params["security"] = "tls" - tlsSetting, _ := stream["tlsSettings"].(map[string]interface{}) - alpns, _ := tlsSetting["alpn"].([]interface{}) + tlsSetting, _ := stream["tlsSettings"].(map[string]any) + alpns, _ := tlsSetting["alpn"].([]any) var alpn []string for _, a := range alpns { alpn = append(alpn, a.(string)) @@ -953,14 +433,15 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin // salamander obfs (Hysteria2). The panel-side link generator already // emits these; keep the subscription output in sync so a client has // the obfs password to match the server. - if finalmask, ok := stream["finalmask"].(map[string]interface{}); ok { - if udpMasks, ok := finalmask["udp"].([]interface{}); ok { + if finalmask, ok := stream["finalmask"].(map[string]any); ok { + applyFinalMaskParams(finalmask, params) + if udpMasks, ok := finalmask["udp"].([]any); ok { for _, m := range udpMasks { - mask, _ := m.(map[string]interface{}) + mask, _ := m.(map[string]any) if mask == nil || mask["type"] != "salamander" { continue } - settings, _ := mask["settings"].(map[string]interface{}) + settings, _ := mask["settings"].(map[string]any) if pw, ok := settings["password"].(string); ok && pw != "" { params["obfs"] = "salamander" params["obfs-password"] = pw @@ -970,7 +451,7 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin } } - var settings map[string]interface{} + var settings map[string]any json.Unmarshal([]byte(inbound.Settings), &settings) version, _ := settings["version"].(float64) protocol := "hysteria2" @@ -983,11 +464,11 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin // server's own IP/port even when the admin configured an alternate // endpoint (e.g. a CDN hostname + port that forwards to the node). // Matches the behaviour of genVlessLink / genTrojanLink / …. - externalProxies, _ := stream["externalProxy"].([]interface{}) + externalProxies, _ := stream["externalProxy"].([]any) if len(externalProxies) > 0 { links := make([]string, 0, len(externalProxies)) for _, externalProxy := range externalProxies { - ep, ok := externalProxy.(map[string]interface{}) + ep, ok := externalProxy.(map[string]any) if !ok { continue } @@ -1023,6 +504,319 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin return url.String() } +func (s *SubService) resolveInboundAddress(inbound *model.Inbound) string { + if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { + return s.address + } + return inbound.Listen +} + +func findClientIndex(clients []model.Client, email string) int { + for i, client := range clients { + if client.Email == email { + return i + } + } + return -1 +} + +func unmarshalStreamSettings(streamSettings string) map[string]any { + var stream map[string]any + json.Unmarshal([]byte(streamSettings), &stream) + return stream +} + +func applyPathAndHostParams(settings map[string]any, params map[string]string) { + params["path"] = settings["path"].(string) + if host, ok := settings["host"].(string); ok && len(host) > 0 { + params["host"] = host + } else { + headers, _ := settings["headers"].(map[string]any) + params["host"] = searchHost(headers) + } +} + +func applyPathAndHostObj(settings map[string]any, obj map[string]any) { + obj["path"] = settings["path"].(string) + if host, ok := settings["host"].(string); ok && len(host) > 0 { + obj["host"] = host + } else { + headers, _ := settings["headers"].(map[string]any) + obj["host"] = searchHost(headers) + } +} + +func applyShareNetworkParams(stream map[string]any, streamNetwork string, params map[string]string) { + switch streamNetwork { + case "tcp": + tcp, _ := stream["tcpSettings"].(map[string]any) + header, _ := tcp["header"].(map[string]any) + typeStr, _ := header["type"].(string) + if typeStr == "http" { + request := header["request"].(map[string]any) + requestPath, _ := request["path"].([]any) + params["path"] = requestPath[0].(string) + headers, _ := request["headers"].(map[string]any) + params["host"] = searchHost(headers) + params["headerType"] = "http" + } + case "kcp": + applyKcpShareParams(stream, params) + case "ws": + ws, _ := stream["wsSettings"].(map[string]any) + applyPathAndHostParams(ws, params) + case "grpc": + grpc, _ := stream["grpcSettings"].(map[string]any) + params["serviceName"] = grpc["serviceName"].(string) + params["authority"], _ = grpc["authority"].(string) + if grpc["multiMode"].(bool) { + params["mode"] = "multi" + } + case "httpupgrade": + httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any) + applyPathAndHostParams(httpupgrade, params) + case "xhttp": + xhttp, _ := stream["xhttpSettings"].(map[string]any) + applyPathAndHostParams(xhttp, params) + params["mode"], _ = xhttp["mode"].(string) + applyXhttpPaddingParams(xhttp, params) + } +} + +func applyXhttpPaddingObj(xhttp map[string]any, obj map[string]any) { + // VMess base64 JSON supports arbitrary keys; copy the padding + // settings through so clients can match the server's xhttp + // xPaddingBytes range and, when the admin opted into obfs + // mode, the custom key / header / placement / method. + if xpb, ok := xhttp["xPaddingBytes"].(string); ok && len(xpb) > 0 { + obj["x_padding_bytes"] = xpb + } + if obfs, ok := xhttp["xPaddingObfsMode"].(bool); ok && obfs { + obj["xPaddingObfsMode"] = true + for _, field := range []string{"xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"} { + if v, ok := xhttp[field].(string); ok && len(v) > 0 { + obj[field] = v + } + } + } +} + +func applyVmessNetworkParams(stream map[string]any, network string, obj map[string]any) { + obj["net"] = network + switch network { + case "tcp": + tcp, _ := stream["tcpSettings"].(map[string]any) + header, _ := tcp["header"].(map[string]any) + typeStr, _ := header["type"].(string) + obj["type"] = typeStr + if typeStr == "http" { + request := header["request"].(map[string]any) + requestPath, _ := request["path"].([]any) + obj["path"] = requestPath[0].(string) + headers, _ := request["headers"].(map[string]any) + obj["host"] = searchHost(headers) + } + case "kcp": + applyKcpShareObj(stream, obj) + case "ws": + ws, _ := stream["wsSettings"].(map[string]any) + applyPathAndHostObj(ws, obj) + case "grpc": + grpc, _ := stream["grpcSettings"].(map[string]any) + obj["path"] = grpc["serviceName"].(string) + obj["authority"] = grpc["authority"].(string) + if grpc["multiMode"].(bool) { + obj["type"] = "multi" + } + case "httpupgrade": + httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any) + applyPathAndHostObj(httpupgrade, obj) + case "xhttp": + xhttp, _ := stream["xhttpSettings"].(map[string]any) + applyPathAndHostObj(xhttp, obj) + obj["mode"], _ = xhttp["mode"].(string) + applyXhttpPaddingObj(xhttp, obj) + } +} + +func applyShareTLSParams(stream map[string]any, params map[string]string) { + params["security"] = "tls" + tlsSetting, _ := stream["tlsSettings"].(map[string]any) + alpns, _ := tlsSetting["alpn"].([]any) + 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) + } + } +} + +func applyVmessTLSParams(stream map[string]any, obj map[string]any) { + tlsSetting, _ := stream["tlsSettings"].(map[string]any) + alpns, _ := tlsSetting["alpn"].([]any) + if len(alpns) > 0 { + var alpn []string + for _, a := range alpns { + alpn = append(alpn, a.(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 fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { + obj["fp"], _ = fpValue.(string) + } + } +} + +func applyShareRealityParams(stream map[string]any, params map[string]string) { + params["security"] = "reality" + realitySetting, _ := stream["realitySettings"].(map[string]any) + realitySettings, _ := searchKey(realitySetting, "settings") + if realitySetting != nil { + if sniValue, ok := searchKey(realitySetting, "serverNames"); ok { + sNames, _ := sniValue.([]any) + params["sni"] = sNames[random.Num(len(sNames))].(string) + } + if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok { + params["pbk"], _ = pbkValue.(string) + } + if sidValue, ok := searchKey(realitySetting, "shortIds"); ok { + shortIds, _ := sidValue.([]any) + params["sid"] = shortIds[random.Num(len(shortIds))].(string) + } + if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok { + if fp, ok := fpValue.(string); ok && len(fp) > 0 { + params["fp"] = fp + } + } + if pqvValue, ok := searchKey(realitySettings, "mldsa65Verify"); ok { + if pqv, ok := pqvValue.(string); ok && len(pqv) > 0 { + params["pqv"] = pqv + } + } + params["spx"] = "/" + random.Seq(15) + } +} + +func buildVmessLink(obj map[string]any) string { + jsonStr, _ := json.MarshalIndent(obj, "", " ") + return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr) +} + +func cloneVmessShareObj(baseObj map[string]any, newSecurity string) map[string]any { + newObj := map[string]any{} + for key, value := range baseObj { + if !(newSecurity == "none" && (key == "alpn" || key == "sni" || key == "fp")) { + newObj[key] = value + } + } + return newObj +} + +func (s *SubService) buildVmessExternalProxyLinks(externalProxies []any, baseObj map[string]any, inbound *model.Inbound, email string) string { + var links strings.Builder + for index, externalProxy := range externalProxies { + ep, _ := externalProxy.(map[string]any) + newSecurity, _ := ep["forceTls"].(string) + newObj := cloneVmessShareObj(baseObj, newSecurity) + 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.WriteString("\n") + } + links.WriteString(buildVmessLink(newObj)) + } + return links.String() +} + +func buildLinkWithParams(link string, params map[string]string, fragment string) string { + parsedURL, _ := url.Parse(link) + q := parsedURL.Query() + for k, v := range params { + q.Add(k, v) + } + parsedURL.RawQuery = q.Encode() + parsedURL.Fragment = fragment + return parsedURL.String() +} + +func buildLinkWithParamsAndSecurity(link string, params map[string]string, fragment, security string, omitTLSFields bool) string { + parsedURL, _ := url.Parse(link) + q := parsedURL.Query() + for k, v := range params { + if k == "security" { + v = security + } + if omitTLSFields && (k == "alpn" || k == "sni" || k == "fp") { + continue + } + q.Add(k, v) + } + parsedURL.RawQuery = q.Encode() + parsedURL.Fragment = fragment + return parsedURL.String() +} + +func (s *SubService) buildExternalProxyURLLinks( + externalProxies []any, + params map[string]string, + baseSecurity string, + makeLink func(dest string, port int) string, + makeRemark func(ep map[string]any) string, +) string { + links := make([]string, 0, len(externalProxies)) + for _, externalProxy := range externalProxies { + ep, _ := externalProxy.(map[string]any) + newSecurity, _ := ep["forceTls"].(string) + dest, _ := ep["dest"].(string) + port := int(ep["port"].(float64)) + + securityToApply := baseSecurity + if newSecurity != "same" { + securityToApply = newSecurity + } + + links = append( + links, + buildLinkWithParamsAndSecurity( + makeLink(dest, port), + params, + makeRemark(ep), + securityToApply, + newSecurity == "none", + ), + ) + } + return strings.Join(links, "\n") +} + +func cloneStringMap(source map[string]string) map[string]string { + cloned := make(map[string]string, len(source)) + maps.Copy(cloned, source) + return cloned +} + func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string { separationChar := string(s.remarkModel[0]) orderChars := s.remarkModel[1:] @@ -1182,6 +976,250 @@ func applyXhttpPaddingParams(xhttp map[string]any, params map[string]string) { } } +var kcpMaskToHeaderType = map[string]string{ + "header-dns": "dns", + "header-dtls": "dtls", + "header-srtp": "srtp", + "header-utp": "utp", + "header-wechat": "wechat-video", + "header-wireguard": "wireguard", +} + +var validFinalMaskUDPTypes = map[string]struct{}{ + "salamander": {}, + "mkcp-aes128gcm": {}, + "header-dns": {}, + "header-dtls": {}, + "header-srtp": {}, + "header-utp": {}, + "header-wechat": {}, + "header-wireguard": {}, + "mkcp-original": {}, + "xdns": {}, + "xicmp": {}, + "header-custom": {}, + "noise": {}, + "sudoku": {}, +} + +// applyKcpShareParams reconstructs legacy KCP share-link fields from either +// the historical kcpSettings.header/seed shape or the current finalmask model. +// This keeps subscription output compatible while avoiding panics when older +// keys are absent from modern inbounds. +func applyKcpShareParams(stream map[string]any, params map[string]string) { + extractKcpShareFields(stream).applyToParams(params) +} + +func applyKcpShareObj(stream map[string]any, obj map[string]any) { + extractKcpShareFields(stream).applyToObj(obj) +} + +type kcpShareFields struct { + headerType string + seed string + mtu int + tti int +} + +func (f kcpShareFields) applyToParams(params map[string]string) { + params["headerType"] = f.headerType + setStringParam(params, "seed", f.seed) + setIntParam(params, "mtu", f.mtu) + setIntParam(params, "tti", f.tti) +} + +func (f kcpShareFields) applyToObj(obj map[string]any) { + obj["type"] = f.headerType + setStringField(obj, "path", f.seed) + setIntField(obj, "mtu", f.mtu) + setIntField(obj, "tti", f.tti) +} + +func extractKcpShareFields(stream map[string]any) kcpShareFields { + fields := kcpShareFields{headerType: "none"} + + if kcp, ok := stream["kcpSettings"].(map[string]any); ok { + if header, ok := kcp["header"].(map[string]any); ok { + if value, ok := header["type"].(string); ok && value != "" { + fields.headerType = value + } + } + if value, ok := kcp["seed"].(string); ok && value != "" { + fields.seed = value + } + if value, ok := readPositiveInt(kcp["mtu"]); ok { + fields.mtu = value + } + if value, ok := readPositiveInt(kcp["tti"]); ok { + fields.tti = value + } + } + + for _, rawMask := range normalizedFinalMaskUDPMasks(stream["finalmask"]) { + mask, _ := rawMask.(map[string]any) + if mask == nil { + continue + } + maskType, _ := mask["type"].(string) + if mapped, ok := kcpMaskToHeaderType[maskType]; ok { + fields.headerType = mapped + continue + } + + switch maskType { + case "mkcp-original": + fields.seed = "" + case "mkcp-aes128gcm": + fields.seed = "" + settings, _ := mask["settings"].(map[string]any) + if value, ok := settings["password"].(string); ok && value != "" { + fields.seed = value + } + } + } + + return fields +} + +func readPositiveInt(value any) (int, bool) { + switch number := value.(type) { + case int: + return number, number > 0 + case int32: + return int(number), number > 0 + case int64: + return int(number), number > 0 + case float32: + parsed := int(number) + return parsed, parsed > 0 + case float64: + parsed := int(number) + return parsed, parsed > 0 + default: + return 0, false + } +} + +func setStringParam(params map[string]string, key, value string) { + if value == "" { + delete(params, key) + return + } + params[key] = value +} + +func setIntParam(params map[string]string, key string, value int) { + if value <= 0 { + delete(params, key) + return + } + params[key] = fmt.Sprintf("%d", value) +} + +func setStringField(obj map[string]any, key, value string) { + if value == "" { + delete(obj, key) + return + } + obj[key] = value +} + +func setIntField(obj map[string]any, key string, value int) { + if value <= 0 { + delete(obj, key) + return + } + obj[key] = value +} + +// applyFinalMaskParams exports the finalmask payload as the compact +// `fm=` share-link field used by v2rayN-compatible clients. +func applyFinalMaskParams(finalmask map[string]any, params map[string]string) { + if fm, ok := marshalFinalMask(finalmask); ok { + params["fm"] = fm + } +} + +func applyFinalMaskObj(finalmask map[string]any, obj map[string]any) { + if fm, ok := marshalFinalMask(finalmask); ok { + obj["fm"] = fm + } +} + +func marshalFinalMask(finalmask map[string]any) (string, bool) { + normalized := normalizeFinalMask(finalmask) + if !hasFinalMaskContent(normalized) { + return "", false + } + b, err := json.Marshal(normalized) + if err != nil || len(b) == 0 || string(b) == "null" { + return "", false + } + return string(b), true +} + +func normalizeFinalMask(finalmask map[string]any) map[string]any { + udpMasks := normalizedFinalMaskUDPMasks(finalmask) + if len(udpMasks) == 0 { + return nil + } + return map[string]any{"udp": udpMasks} +} + +func normalizedFinalMaskUDPMasks(value any) []any { + finalmask, _ := value.(map[string]any) + if finalmask == nil { + return nil + } + rawMasks, _ := finalmask["udp"].([]any) + if len(rawMasks) == 0 { + return nil + } + + normalized := make([]any, 0, len(rawMasks)) + for _, rawMask := range rawMasks { + mask, _ := rawMask.(map[string]any) + if mask == nil { + continue + } + maskType, _ := mask["type"].(string) + if _, ok := validFinalMaskUDPTypes[maskType]; !ok || maskType == "" { + continue + } + + normalizedMask := map[string]any{"type": maskType} + if settings, ok := mask["settings"].(map[string]any); ok && len(settings) > 0 { + normalizedMask["settings"] = settings + } + normalized = append(normalized, normalizedMask) + } + + if len(normalized) == 0 { + return nil + } + return normalized +} + +func hasFinalMaskContent(value any) bool { + switch v := value.(type) { + case nil: + return false + case string: + return len(v) > 0 + case map[string]any: + for _, item := range v { + if hasFinalMaskContent(item) { + return true + } + } + return false + case []any: + return slices.ContainsFunc(v, hasFinalMaskContent) + default: + return true + } +} + func searchHost(headers any) string { data, _ := headers.(map[string]any) for k, v := range data { diff --git a/web/assets/js/model/inbound.js b/web/assets/js/model/inbound.js index a091e1ef..ef9ad19c 100644 --- a/web/assets/js/model/inbound.js +++ b/web/assets/js/model/inbound.js @@ -1370,6 +1370,50 @@ class Inbound extends XrayCommonClass { } } + static hasShareableFinalMaskValue(value) { + if (value == null) { + return false; + } + if (Array.isArray(value)) { + return value.some(item => Inbound.hasShareableFinalMaskValue(item)); + } + if (typeof value === 'object') { + return Object.values(value).some(item => Inbound.hasShareableFinalMaskValue(item)); + } + if (typeof value === 'string') { + return value.length > 0; + } + return true; + } + + static serializeFinalMask(finalmask) { + if (!finalmask) { + return ''; + } + const value = typeof finalmask.toJson === 'function' ? finalmask.toJson() : finalmask; + return Inbound.hasShareableFinalMaskValue(value) ? JSON.stringify(value) : ''; + } + + // Export finalmask with the same compact JSON payload shape that + // v2rayN-compatible share links use: fm=. + static applyFinalMaskToParams(finalmask, params) { + if (!params) return; + const payload = Inbound.serializeFinalMask(finalmask); + if (payload.length > 0) { + params.set("fm", payload); + } + } + + // VMess links are a base64 JSON object, so keep the same fm payload + // under a flat property instead of a URL query string. + static applyFinalMaskToObj(finalmask, obj) { + if (!obj) return; + const payload = Inbound.serializeFinalMask(finalmask); + if (payload.length > 0) { + obj.fm = payload; + } + } + get clients() { switch (this.protocol) { case Protocols.VMESS: return this.settings.vmesses; @@ -1590,6 +1634,8 @@ class Inbound extends XrayCommonClass { Inbound.applyXhttpPaddingToObj(xhttp, obj); } + Inbound.applyFinalMaskToObj(this.stream.finalmask, obj); + if (tls === 'tls') { if (!ObjectUtil.isEmpty(this.stream.tls.sni)) { obj.sni = this.stream.tls.sni; @@ -1658,6 +1704,8 @@ class Inbound extends XrayCommonClass { break; } + Inbound.applyFinalMaskToParams(this.stream.finalmask, params); + if (security === 'tls') { params.set("security", "tls"); if (this.stream.isTls) { @@ -1761,6 +1809,8 @@ class Inbound extends XrayCommonClass { break; } + Inbound.applyFinalMaskToParams(this.stream.finalmask, params); + if (security === 'tls') { params.set("security", "tls"); if (this.stream.isTls) { @@ -1840,6 +1890,8 @@ class Inbound extends XrayCommonClass { break; } + Inbound.applyFinalMaskToParams(this.stream.finalmask, params); + if (security === 'tls') { params.set("security", "tls"); if (this.stream.isTls) { @@ -1907,6 +1959,8 @@ class Inbound extends XrayCommonClass { } } + Inbound.applyFinalMaskToParams(this.stream.finalmask, params); + const url = new URL(link); for (const [key, value] of params) { url.searchParams.set(key, value); diff --git a/web/assets/js/model/outbound.js b/web/assets/js/model/outbound.js index 8db9d8e2..a84c0318 100644 --- a/web/assets/js/model/outbound.js +++ b/web/assets/js/model/outbound.js @@ -992,6 +992,10 @@ class Outbound extends CommonClass { stream.kcp = new KcpStreamSettings(); stream.type = json.type; stream.seed = json.path; + const mtu = Number(json.mtu); + if (Number.isFinite(mtu) && mtu > 0) stream.kcp.mtu = mtu; + const tti = Number(json.tti); + if (Number.isFinite(tti) && tti > 0) stream.kcp.tti = tti; } else if (network === 'ws') { stream.ws = new WsStreamSettings(json.path, json.host); } else if (network === 'grpc') { @@ -1029,6 +1033,7 @@ class Outbound extends CommonClass { let headerType = url.searchParams.get('headerType') ?? undefined; let host = url.searchParams.get('host') ?? undefined; let path = url.searchParams.get('path') ?? undefined; + let seed = url.searchParams.get('seed') ?? path ?? undefined; let mode = url.searchParams.get('mode') ?? undefined; if (type === 'tcp' || type === 'none') { @@ -1036,7 +1041,11 @@ class Outbound extends CommonClass { } else if (type === 'kcp') { stream.kcp = new KcpStreamSettings(); stream.kcp.type = headerType ?? 'none'; - stream.kcp.seed = path; + stream.kcp.seed = seed; + const mtu = Number(url.searchParams.get('mtu')); + if (Number.isFinite(mtu) && mtu > 0) stream.kcp.mtu = mtu; + const tti = Number(url.searchParams.get('tti')); + if (Number.isFinite(tti) && tti > 0) stream.kcp.tti = tti; } else if (type === 'ws') { stream.ws = new WsStreamSettings(path, host); } else if (type === 'grpc') { diff --git a/web/html/settings/panel/subscription/subpage.html b/web/html/settings/panel/subscription/subpage.html index 64c1224d..2b3c7939 100644 --- a/web/html/settings/panel/subscription/subpage.html +++ b/web/html/settings/panel/subscription/subpage.html @@ -6,6 +6,23 @@