Files
3x-ui/internal/sub/endpoint.go
T
Sanaei 4e423fa452 fix(sub): drop Reality parameters when a host forces plain TLS
A Host row may set Security to tls on an inbound whose own stream is Reality.
The emitted link then carried security=tls next to pbk, sid, spx and the
Reality dest as sni: the endpoint no longer performs a Reality handshake, so
those describe a server the client will never reach, and clients that honour
them fail to connect. Only the security key was rewritten at emit time, and the
existing strip covered alpn/sni/fp/pcs for forceTls=none alone.

Clear the Reality-only parameters before the endpoint's own TLS overrides are
applied, so a host that supplies its own sni or fingerprint still wins.

Closes #6424
2026-09-09 00:37:31 +02:00

145 lines
5.0 KiB
Go

package sub
import (
"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
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.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",
}
}
// 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)
links = append(links, buildLinkWithParamsAndSecurity(
makeLink(e),
nextParams,
makeRemark(e),
securityToApply,
e.ForceTls == "none",
))
}
return strings.Join(links, "\n")
}
// 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
}
applyEndpointTLSObj(e, newObj, securityToApply)
applyEndpointHostPathObj(e, newObj)
applyEndpointFinalMaskObj(e, newObj)
if index > 0 {
links.WriteString("\n")
}
links.WriteString(buildVmessLink(newObj))
}
return links.String()
}