mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-01 19:52:09 +03:00
aee45ca3fe
Invariant: an inbound's enabled Hosts are the endpoints every client config for it advertises, whichever surface renders that config. WireGuard and AmneziaWG broke it. Their raw generators ignored the externalProxy entries Hosts are injected as and always emitted resolveInboundAddress, so the raw subscription, the sub page .conf, the clients links API and "export all links" gave out the panel address while the JSON and Clash formats of the same inbound used the Host. advertisedEndpoints now states the fan-out once for mtproto, wireguard and amneziawg. The browser-built configs had the same gap. The Clients page WireGuard and AmneziaWG config blocks and QR panels, and its TUIC Clash config, used the panel hostname next to server links that already used Hosts; the Inbounds page peer configs, QR and export ignored them too. withMtprotoHostEndpoints becomes withHostEndpoints over a shared hostEndpointsFor mirror of the backend, the tunnel fan-outs render one config per Host, and the clients page waits for the hosts list the way the inbounds page does, so an empty list means "no hosts" rather than "not loaded yet".
181 lines
6.3 KiB
Go
181 lines
6.3 KiB
Go
package sub
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"strings"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
)
|
|
|
|
// ShareEndpoint is one render target for a subscription link: the address/port
|
|
// to dial plus an optional set of TLS overrides. It unifies two sources behind
|
|
// one type so the per-protocol link builders don't branch on where the override
|
|
// came from:
|
|
//
|
|
// - a legacy externalProxy entry (Phase 1): the source map is carried in `ep`
|
|
// and applied through the unchanged applyExternalProxyTLS* helpers, so the
|
|
// emitted link is byte-identical to the pre-refactor output;
|
|
// - a Host row (Phase 4): leaves `ep` nil and uses typed override fields.
|
|
//
|
|
// ForceTls is the verbatim "same"/"tls"/"none"/"" value — never pre-resolved,
|
|
// because three behaviors branch on the raw string (keep-base, obj["tls"]
|
|
// rewrite, none-strip).
|
|
type ShareEndpoint struct {
|
|
Address string
|
|
Port int
|
|
Remark string // extra remark slot fed to genRemark, not a rendered remark
|
|
ServerDescription string // subtitle caption displayed in Happ client
|
|
ForceTls string
|
|
|
|
// ep is the source externalProxy entry. nil for host/default endpoints.
|
|
ep map[string]any
|
|
}
|
|
|
|
// externalProxyToEndpoint maps one externalProxy entry to an endpoint that
|
|
// carries the entry for delegated, provably-identical TLS application.
|
|
func externalProxyToEndpoint(ep map[string]any) ShareEndpoint {
|
|
e := ShareEndpoint{ep: ep}
|
|
e.Address, _ = ep["dest"].(string)
|
|
if p, ok := ep["port"].(float64); ok {
|
|
e.Port = int(p)
|
|
}
|
|
e.Remark, _ = ep["remark"].(string)
|
|
e.ServerDescription, _ = ep["serverDescription"].(string)
|
|
e.ForceTls, _ = ep["forceTls"].(string)
|
|
return e
|
|
}
|
|
|
|
// inboundDefaultEndpoint is the endpoint for an inbound's own resolved
|
|
// address/port (the no-externalProxy default). forceTls "same" keeps the base
|
|
// security; no per-endpoint TLS override.
|
|
func (s *SubService) inboundDefaultEndpoint(inbound *model.Inbound) ShareEndpoint {
|
|
return ShareEndpoint{
|
|
Address: s.resolveInboundAddress(inbound),
|
|
Port: inbound.Port,
|
|
ForceTls: "same",
|
|
}
|
|
}
|
|
|
|
// advertisedEndpoints is every endpoint a stream-less link (mtproto, wireguard,
|
|
// amneziawg) must fan out over: the externalProxy/Host entries, else the default.
|
|
func (s *SubService) advertisedEndpoints(inbound *model.Inbound) []ShareEndpoint {
|
|
stream := unmarshalStreamSettings(inbound.StreamSettings)
|
|
if externalProxies, ok := stream["externalProxy"].([]any); ok {
|
|
endpoints := make([]ShareEndpoint, 0, len(externalProxies))
|
|
for _, raw := range externalProxies {
|
|
if ep, ok := raw.(map[string]any); ok {
|
|
endpoints = append(endpoints, externalProxyToEndpoint(ep))
|
|
}
|
|
}
|
|
if len(endpoints) > 0 {
|
|
return endpoints
|
|
}
|
|
}
|
|
return []ShareEndpoint{s.inboundDefaultEndpoint(inbound)}
|
|
}
|
|
|
|
// applyEndpointTLSParams applies an endpoint's TLS overrides onto a URL-param
|
|
// map. External-proxy endpoints delegate to the unchanged helper; host/default
|
|
// endpoints carry no override yet (Phase 4).
|
|
func applyEndpointTLSParams(e ShareEndpoint, params map[string]string, security string) {
|
|
if e.ep != nil {
|
|
applyExternalProxyTLSParams(e.ep, params, security)
|
|
}
|
|
}
|
|
|
|
// applyEndpointTLSObj is applyEndpointTLSParams for the VMess base64-JSON form.
|
|
func applyEndpointTLSObj(e ShareEndpoint, obj map[string]any, security string) {
|
|
if e.ep != nil {
|
|
applyExternalProxyTLSObj(e.ep, obj, security)
|
|
}
|
|
}
|
|
|
|
// dropBaseRealityParams removes the parameters that only mean something on a
|
|
// reality link once a host forces the endpoint to plain TLS or no TLS.
|
|
func dropBaseRealityParams(params map[string]string, baseSecurity, securityToApply string) {
|
|
if baseSecurity != "reality" || securityToApply == "reality" {
|
|
return
|
|
}
|
|
// sni and fp name the master's reality dest, not this endpoint's own
|
|
// certificate; the host's values are re-applied right after this.
|
|
for _, k := range []string{"pbk", "sid", "spx", "pqv", "sni", "fp"} {
|
|
delete(params, k)
|
|
}
|
|
}
|
|
|
|
// buildEndpointLinks renders one URL-param link per endpoint (vless/trojan/ss).
|
|
// securityToApply mirrors the legacy externalProxy loop: "same" keeps the base
|
|
// security, otherwise the endpoint's forceTls wins; "none" strips TLS hint
|
|
// fields at emit time.
|
|
func (s *SubService) buildEndpointLinks(
|
|
eps []ShareEndpoint,
|
|
params map[string]string,
|
|
baseSecurity string,
|
|
makeLink func(e ShareEndpoint) string,
|
|
makeRemark func(e ShareEndpoint) string,
|
|
) string {
|
|
links := make([]string, 0, len(eps))
|
|
for _, e := range eps {
|
|
securityToApply := baseSecurity
|
|
if e.ForceTls != "same" {
|
|
securityToApply = e.ForceTls
|
|
}
|
|
nextParams := cloneStringMap(params)
|
|
dropBaseRealityParams(nextParams, baseSecurity, securityToApply)
|
|
applyEndpointTLSParams(e, nextParams, securityToApply)
|
|
applyEndpointRealityParams(e, nextParams, securityToApply)
|
|
applyEndpointHostPath(e, nextParams)
|
|
applyEndpointFinalMask(e, nextParams)
|
|
applyEndpointAllowInsecure(e, nextParams, securityToApply)
|
|
remark := makeRemark(e)
|
|
if e.ServerDescription != "" {
|
|
remark = appendHappServerDescription(remark, e.ServerDescription)
|
|
}
|
|
links = append(links, buildLinkWithParamsAndSecurity(
|
|
makeLink(e),
|
|
nextParams,
|
|
remark,
|
|
securityToApply,
|
|
e.ForceTls == "none",
|
|
))
|
|
}
|
|
return strings.Join(links, "\n")
|
|
}
|
|
|
|
func appendHappServerDescription(remark, desc string) string {
|
|
if desc == "" {
|
|
return remark
|
|
}
|
|
encoded := base64.StdEncoding.EncodeToString([]byte(desc))
|
|
return remark + "?serverDescription=" + encoded
|
|
}
|
|
|
|
// buildEndpointVmessLinks renders one VMess base64-JSON link per endpoint.
|
|
func (s *SubService) buildEndpointVmessLinks(eps []ShareEndpoint, baseObj map[string]any, inbound *model.Inbound, email string, transport string) string {
|
|
var links strings.Builder
|
|
for index, e := range eps {
|
|
securityToApply, _ := baseObj["tls"].(string)
|
|
if e.ForceTls != "same" {
|
|
securityToApply = e.ForceTls
|
|
}
|
|
newObj := cloneVmessShareObj(baseObj, e.ForceTls)
|
|
newObj["ps"] = s.endpointRemark(inbound, email, e.ep, transport)
|
|
newObj["add"] = e.Address
|
|
newObj["port"] = e.Port
|
|
if e.ForceTls != "same" {
|
|
newObj["tls"] = e.ForceTls
|
|
}
|
|
if e.ServerDescription != "" {
|
|
newObj["serverDescription"] = e.ServerDescription
|
|
}
|
|
applyEndpointTLSObj(e, newObj, securityToApply)
|
|
applyEndpointHostPathObj(e, newObj)
|
|
applyEndpointFinalMaskObj(e, newObj)
|
|
if index > 0 {
|
|
links.WriteString("\n")
|
|
}
|
|
links.WriteString(buildVmessLink(newObj))
|
|
}
|
|
return links.String()
|
|
}
|