3x-ui/sub/subJsonService.go
MHSanaei f8e902a7b6
fix(sub): include ECH config in TLS share links and JSON subscription
echConfigList was stored under tlsSettings.settings but the share-link
and JSON-subscription generators only read fingerprint and
pinnedPeerCertSha256 from that bag, silently dropping ECH from VLESS,
Trojan and VMess links. Read echConfigList alongside them and flatten it
into tlsSettings.echConfigList for the JSON subscription.

Closes #4933
2026-06-05 00:20:29 +02:00

495 lines
15 KiB
Go

package sub
import (
_ "embed"
"encoding/json"
"fmt"
"maps"
"strings"
"github.com/mhsanaei/3x-ui/v3/database/model"
"github.com/mhsanaei/3x-ui/v3/logger"
"github.com/mhsanaei/3x-ui/v3/util/json_util"
"github.com/mhsanaei/3x-ui/v3/util/random"
"github.com/mhsanaei/3x-ui/v3/web/service"
)
//go:embed default.json
var defaultJson string
// SubJsonService handles JSON subscription configuration generation and management.
type SubJsonService struct {
configJson map[string]any
defaultOutbounds []json_util.RawMessage
finalMask string
mux string
inboundService service.InboundService
SubService *SubService
}
// NewSubJsonService creates a new JSON subscription service with the given configuration.
func NewSubJsonService(mux string, rules string, finalMask string, subService *SubService) *SubJsonService {
var configJson map[string]any
var defaultOutbounds []json_util.RawMessage
json.Unmarshal([]byte(defaultJson), &configJson)
if outboundSlices, ok := configJson["outbounds"].([]any); ok {
for _, defaultOutbound := range outboundSlices {
jsonBytes, _ := json.Marshal(defaultOutbound)
defaultOutbounds = append(defaultOutbounds, jsonBytes)
}
}
if rules != "" {
var newRules []any
routing, _ := configJson["routing"].(map[string]any)
defaultRules, _ := routing["rules"].([]any)
json.Unmarshal([]byte(rules), &newRules)
defaultRules = append(newRules, defaultRules...)
routing["rules"] = defaultRules
configJson["routing"] = routing
}
return &SubJsonService{
configJson: configJson,
defaultOutbounds: defaultOutbounds,
finalMask: finalMask,
mux: mux,
SubService: subService,
}
}
// GetJson generates a JSON subscription configuration for the given subscription ID and host.
func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) {
// Set per-request state on the shared SubService so any
// resolveInboundAddress call inside picks node-aware host values.
s.SubService.PrepareForRequest(host)
inbounds, err := s.SubService.getInboundsBySubId(subId)
if err != nil || len(inbounds) == 0 {
return "", "", err
}
var header string
var configArray []json_util.RawMessage
seenEmails := make(map[string]struct{})
// Prepare Inbounds
for _, inbound := range inbounds {
clients, err := s.inboundService.GetClients(inbound)
if err != nil {
logger.Error("SubJsonService - GetClients: Unable to get clients from inbound")
}
if clients == nil {
continue
}
s.SubService.projectThroughFallbackMaster(inbound)
for _, client := range clients {
if client.SubID == subId {
seenEmails[client.Email] = struct{}{}
configArray = append(configArray, s.getConfig(inbound, client, host)...)
}
}
}
if len(configArray) == 0 {
return "", "", nil
}
emails := make([]string, 0, len(seenEmails))
for e := range seenEmails {
emails = append(emails, e)
}
traffic, _ := s.SubService.AggregateTrafficByEmails(emails)
// Combile outbounds
var finalJson []byte
if len(configArray) == 1 {
finalJson, _ = json.MarshalIndent(configArray[0], "", " ")
} else {
finalJson, _ = json.MarshalIndent(configArray, "", " ")
}
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
return string(finalJson), header, nil
}
func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client, host string) []json_util.RawMessage {
var newJsonArray []json_util.RawMessage
stream := s.streamData(inbound.StreamSettings)
// When externalProxy is empty the JSON config falls back to a
// synthetic one whose `dest` is the host the client connects to.
// For node-managed inbounds we want the node's address — request
// host won't reach the right xray. resolveInboundAddress already
// implements the node→subscriber-host fallback chain.
defaultDest := s.SubService.resolveInboundAddress(inbound)
if defaultDest == "" {
defaultDest = host
}
externalProxies, ok := stream["externalProxy"].([]any)
hasExternalProxy := ok && len(externalProxies) > 0
if !hasExternalProxy {
externalProxies = []any{
map[string]any{
"forceTls": "same",
"dest": defaultDest,
"port": float64(inbound.Port),
"remark": "",
},
}
}
delete(stream, "externalProxy")
for _, ep := range externalProxies {
extPrxy := ep.(map[string]any)
inbound.Listen = extPrxy["dest"].(string)
inbound.Port = int(extPrxy["port"].(float64))
newStream := cloneStreamForExternalProxy(stream)
switch extPrxy["forceTls"].(string) {
case "tls":
if newStream["security"] != "tls" {
newStream["security"] = "tls"
newStream["tlsSettings"] = map[string]any{}
}
case "none":
if newStream["security"] != "none" {
newStream["security"] = "none"
delete(newStream, "tlsSettings")
}
}
security, _ := newStream["security"].(string)
if hasExternalProxy {
applyExternalProxyTLSToStream(extPrxy, newStream, security)
}
streamSettings, _ := json.MarshalIndent(newStream, "", " ")
var newOutbounds []json_util.RawMessage
switch inbound.Protocol {
case "vmess":
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client))
case "vless":
newOutbounds = append(newOutbounds, s.genVless(inbound, streamSettings, client))
case "trojan", "shadowsocks":
newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
case "hysteria":
newOutbounds = append(newOutbounds, s.genHy(inbound, newStream, client))
}
newOutbounds = append(newOutbounds, s.defaultOutbounds...)
newConfigJson := make(map[string]any)
maps.Copy(newConfigJson, s.configJson)
newConfigJson["outbounds"] = newOutbounds
newConfigJson["remarks"] = s.SubService.genRemark(inbound, client.Email, extPrxy["remark"].(string))
newConfig, _ := json.MarshalIndent(newConfigJson, "", " ")
newJsonArray = append(newJsonArray, newConfig)
}
return newJsonArray
}
func (s *SubJsonService) streamData(stream string) map[string]any {
var streamSettings map[string]any
json.Unmarshal([]byte(stream), &streamSettings)
security, _ := streamSettings["security"].(string)
switch security {
case "tls":
streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]any))
case "reality":
streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]any))
}
delete(streamSettings, "sockopt")
if s.finalMask != "" {
s.applyGlobalFinalMask(streamSettings)
}
// remove proxy protocol
network, _ := streamSettings["network"].(string)
switch network {
case "tcp":
streamSettings["tcpSettings"] = s.removeAcceptProxy(streamSettings["tcpSettings"])
case "ws":
streamSettings["wsSettings"] = s.removeAcceptProxy(streamSettings["wsSettings"])
case "httpupgrade":
streamSettings["httpupgradeSettings"] = s.removeAcceptProxy(streamSettings["httpupgradeSettings"])
case "xhttp":
streamSettings["xhttpSettings"] = s.removeAcceptProxy(streamSettings["xhttpSettings"])
if xhttp, ok := streamSettings["xhttpSettings"].(map[string]any); ok {
delete(xhttp, "noSSEHeader")
delete(xhttp, "scMaxBufferedPosts")
delete(xhttp, "scStreamUpServerSecs")
delete(xhttp, "serverMaxHeaderBytes")
}
}
return streamSettings
}
func (s *SubJsonService) applyGlobalFinalMask(streamSettings map[string]any) {
var fm map[string]any
if err := json.Unmarshal([]byte(s.finalMask), &fm); err != nil || len(fm) == 0 {
return
}
merged := mergeFinalMask(streamSettings["finalmask"], fm)
if len(merged) > 0 {
streamSettings["finalmask"] = merged
}
}
func (s *SubJsonService) removeAcceptProxy(setting any) map[string]any {
netSettings, ok := setting.(map[string]any)
if ok {
delete(netSettings, "acceptProxyProtocol")
}
return netSettings
}
func (s *SubJsonService) tlsData(tData map[string]any) map[string]any {
tlsData := make(map[string]any, 1)
tlsClientSettings, _ := tData["settings"].(map[string]any)
tlsData["serverName"] = tData["serverName"]
tlsData["alpn"] = tData["alpn"]
if fingerprint, ok := tlsClientSettings["fingerprint"].(string); ok {
tlsData["fingerprint"] = fingerprint
}
if ech, ok := tlsClientSettings["echConfigList"].(string); ok && ech != "" {
tlsData["echConfigList"] = ech
}
if pins, ok := tlsClientSettings["pinnedPeerCertSha256"].([]any); ok && len(pins) > 0 {
tlsData["pinnedPeerCertSha256"] = pins
}
return tlsData
}
func (s *SubJsonService) realityData(rData map[string]any) map[string]any {
rltyData := make(map[string]any, 1)
rltyClientSettings, _ := rData["settings"].(map[string]any)
rltyData["show"] = false
rltyData["publicKey"] = rltyClientSettings["publicKey"]
rltyData["fingerprint"] = rltyClientSettings["fingerprint"]
rltyData["mldsa65Verify"] = rltyClientSettings["mldsa65Verify"]
// Set random data
rltyData["spiderX"] = "/" + random.Seq(15)
shortIds, ok := rData["shortIds"].([]any)
if ok && len(shortIds) > 0 {
rltyData["shortId"] = shortIds[random.Num(len(shortIds))].(string)
} else {
rltyData["shortId"] = ""
}
serverNames, ok := rData["serverNames"].([]any)
if ok && len(serverNames) > 0 {
rltyData["serverName"] = serverNames[random.Num(len(serverNames))].(string)
} else {
rltyData["serverName"] = ""
}
return rltyData
}
func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
outbound := Outbound{}
outbound.Protocol = string(inbound.Protocol)
outbound.Tag = "proxy"
if s.mux != "" {
outbound.Mux = json_util.RawMessage(s.mux)
}
outbound.StreamSettings = streamSettings
security := client.Security
if security == "" {
security = "auto"
}
outbound.Settings = map[string]any{
"address": inbound.Listen,
"port": inbound.Port,
"id": client.ID,
"security": security,
"level": 8,
}
result, _ := json.MarshalIndent(outbound, "", " ")
return result
}
func (s *SubJsonService) genVless(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
outbound := Outbound{}
outbound.Protocol = string(inbound.Protocol)
outbound.Tag = "proxy"
if s.mux != "" {
outbound.Mux = json_util.RawMessage(s.mux)
}
outbound.StreamSettings = streamSettings
// Add encryption for VLESS outbound from inbound settings
var inboundSettings map[string]any
json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
encryption, _ := inboundSettings["encryption"].(string)
settings := map[string]any{
"address": inbound.Listen,
"port": inbound.Port,
"id": client.ID,
"encryption": encryption,
"level": 8,
}
if client.Flow != "" {
settings["flow"] = client.Flow
}
outbound.Settings = settings
result, _ := json.MarshalIndent(outbound, "", " ")
return result
}
func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
outbound := Outbound{}
serverData := make([]ServerSetting, 1)
serverData[0] = ServerSetting{
Address: inbound.Listen,
Port: inbound.Port,
Level: 8,
Password: client.Password,
}
if inbound.Protocol == model.Shadowsocks {
var inboundSettings map[string]any
json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
method, _ := inboundSettings["method"].(string)
serverData[0].Method = method
// server password in multi-user 2022 protocols
if strings.HasPrefix(method, "2022") {
if serverPassword, ok := inboundSettings["password"].(string); ok {
serverData[0].Password = fmt.Sprintf("%s:%s", serverPassword, client.Password)
}
}
}
outbound.Protocol = string(inbound.Protocol)
outbound.Tag = "proxy"
if s.mux != "" {
outbound.Mux = json_util.RawMessage(s.mux)
}
outbound.StreamSettings = streamSettings
settings := map[string]any{
"address": serverData[0].Address,
"port": serverData[0].Port,
"password": serverData[0].Password,
"level": 8,
}
if inbound.Protocol == model.Shadowsocks {
settings["method"] = serverData[0].Method
}
outbound.Settings = settings
result, _ := json.MarshalIndent(outbound, "", " ")
return result
}
func (s *SubJsonService) genHy(inbound *model.Inbound, newStream map[string]any, client model.Client) json_util.RawMessage {
outbound := Outbound{}
outbound.Protocol = string(inbound.Protocol)
outbound.Tag = "proxy"
if s.mux != "" {
outbound.Mux = json_util.RawMessage(s.mux)
}
var settings, stream map[string]any
json.Unmarshal([]byte(inbound.Settings), &settings)
version, _ := settings["version"].(float64)
outbound.Settings = map[string]any{
"version": int(version),
"address": inbound.Listen,
"port": inbound.Port,
}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
hyStream := stream["hysteriaSettings"].(map[string]any)
outHyStream := map[string]any{
"version": int(version),
"auth": client.Auth,
}
if udpIdleTimeout, ok := hyStream["udpIdleTimeout"].(float64); ok {
outHyStream["udpIdleTimeout"] = int(udpIdleTimeout)
}
if masquerade, ok := hyStream["masquerade"].(map[string]any); ok {
outHyStream["masquerade"] = masquerade
}
newStream["hysteriaSettings"] = outHyStream
if finalmask, ok := hyStream["finalmask"].(map[string]any); ok {
newStream["finalmask"] = mergeFinalMask(newStream["finalmask"], finalmask)
}
newStream["network"] = "hysteria"
newStream["security"] = "tls"
outbound.StreamSettings, _ = json.MarshalIndent(newStream, "", " ")
result, _ := json.MarshalIndent(outbound, "", " ")
return result
}
func mergeFinalMask(base any, extra map[string]any) map[string]any {
merged := map[string]any{}
if baseMap, ok := base.(map[string]any); ok {
for key, value := range baseMap {
switch key {
case "tcp", "udp":
if masks, ok := value.([]any); ok {
merged[key] = append([]any(nil), masks...)
}
default:
merged[key] = value
}
}
}
for key, value := range extra {
switch key {
case "tcp", "udp":
baseMasks, _ := merged[key].([]any)
extraMasks, _ := value.([]any)
if len(extraMasks) > 0 {
merged[key] = append(baseMasks, extraMasks...)
}
case "quicParams":
if _, exists := merged[key]; !exists {
merged[key] = value
}
default:
merged[key] = value
}
}
return merged
}
type Outbound struct {
Protocol string `json:"protocol"`
Tag string `json:"tag"`
StreamSettings json_util.RawMessage `json:"streamSettings"`
Mux json_util.RawMessage `json:"mux,omitempty"`
Settings map[string]any `json:"settings,omitempty"`
}
type ServerSetting struct {
Password string `json:"password"`
Level int `json:"level"`
Address string `json:"address"`
Port int `json:"port"`
Flow string `json:"flow,omitempty"`
Method string `json:"method,omitempty"`
}