mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-03 10:59:34 +00:00
The subscription page leaked an inbound's server-side Listen IP into the client-facing URLs when a bind address was set: - Per-config links: resolveInboundAddress returned the bind Listen IP (loopback/private/public alike) instead of the host the subscriber reached the panel on. It now returns the node address for node-managed inbounds, otherwise the subscriber host; the bind Listen is ignored (External Proxy remains the way to advertise a specific endpoint). - Subscription Copy URL (SUB/JSON/CLASH): BuildURLs composed the base differently from the panel's Client Information page and never normalized the request host, so a loopback/bind request leaked the raw IP. The composition is extracted into the shared SettingService.BuildSubURIBase, used by both the panel and the sub page so they render identically, and fed the already-normalized subscriber host.
485 lines
14 KiB
Go
485 lines
14 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
|
|
fragmentOrNoises bool
|
|
mux string
|
|
|
|
inboundService service.InboundService
|
|
SubService *SubService
|
|
}
|
|
|
|
// NewSubJsonService creates a new JSON subscription service with the given configuration.
|
|
func NewSubJsonService(fragment string, noises string, mux string, rules 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)
|
|
}
|
|
}
|
|
|
|
fragmentOrNoises := false
|
|
if fragment != "" || noises != "" {
|
|
fragmentOrNoises = true
|
|
defaultOutboundsSettings := map[string]any{
|
|
"domainStrategy": "UseIP",
|
|
"redirect": "",
|
|
}
|
|
|
|
if fragment != "" {
|
|
defaultOutboundsSettings["fragment"] = json_util.RawMessage(fragment)
|
|
}
|
|
|
|
if noises != "" {
|
|
defaultOutboundsSettings["noises"] = json_util.RawMessage(noises)
|
|
}
|
|
|
|
defaultDirectOutbound := map[string]any{
|
|
"protocol": "freedom",
|
|
"settings": defaultOutboundsSettings,
|
|
"tag": "direct_out",
|
|
}
|
|
jsonBytes, _ := json.MarshalIndent(defaultDirectOutbound, "", " ")
|
|
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,
|
|
fragmentOrNoises: fragmentOrNoises,
|
|
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.fragmentOrNoises {
|
|
streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "direct_out", "tcpKeepAliveIdle": 100}`)
|
|
}
|
|
|
|
// 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) 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 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{}
|
|
usersData := make([]UserVnext, 1)
|
|
|
|
usersData[0].ID = client.ID
|
|
usersData[0].Email = client.Email
|
|
usersData[0].Security = client.Security
|
|
vnextData := make([]VnextSetting, 1)
|
|
vnextData[0] = VnextSetting{
|
|
Address: inbound.Listen,
|
|
Port: inbound.Port,
|
|
Users: usersData,
|
|
}
|
|
|
|
outbound.Protocol = string(inbound.Protocol)
|
|
outbound.Tag = "proxy"
|
|
if s.mux != "" {
|
|
outbound.Mux = json_util.RawMessage(s.mux)
|
|
}
|
|
outbound.StreamSettings = streamSettings
|
|
outbound.Settings = map[string]any{
|
|
"vnext": vnextData,
|
|
}
|
|
|
|
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)
|
|
|
|
user := map[string]any{
|
|
"id": client.ID,
|
|
"level": 8,
|
|
"encryption": encryption,
|
|
}
|
|
if client.Flow != "" {
|
|
user["flow"] = client.Flow
|
|
}
|
|
|
|
vnext := map[string]any{
|
|
"address": inbound.Listen,
|
|
"port": inbound.Port,
|
|
"users": []any{user},
|
|
}
|
|
|
|
outbound.Settings = map[string]any{
|
|
"vnext": []any{vnext},
|
|
}
|
|
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
|
|
outbound.Settings = map[string]any{
|
|
"servers": serverData,
|
|
}
|
|
|
|
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"] = finalmask
|
|
}
|
|
|
|
newStream["network"] = "hysteria"
|
|
newStream["security"] = "tls"
|
|
|
|
outbound.StreamSettings, _ = json.MarshalIndent(newStream, "", " ")
|
|
|
|
result, _ := json.MarshalIndent(outbound, "", " ")
|
|
return result
|
|
}
|
|
|
|
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 VnextSetting struct {
|
|
Address string `json:"address"`
|
|
Port int `json:"port"`
|
|
Users []UserVnext `json:"users"`
|
|
}
|
|
|
|
type UserVnext struct {
|
|
ID string `json:"id"`
|
|
Email string `json:"email,omitempty"`
|
|
Security string `json:"security,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"`
|
|
}
|