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
This commit is contained in:
Sanaei
2026-09-09 00:37:31 +02:00
parent 65c5580e7d
commit 4e423fa452
2 changed files with 38 additions and 0 deletions
+14
View File
@@ -69,6 +69,19 @@ func applyEndpointTLSObj(e ShareEndpoint, obj map[string]any, security string) {
}
}
// 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
@@ -87,6 +100,7 @@ func (s *SubService) buildEndpointLinks(
securityToApply = e.ForceTls
}
nextParams := cloneStringMap(params)
dropBaseRealityParams(nextParams, baseSecurity, securityToApply)
applyEndpointTLSParams(e, nextParams, securityToApply)
applyEndpointRealityParams(e, nextParams, securityToApply)
applyEndpointHostPath(e, nextParams)
+24
View File
@@ -418,3 +418,27 @@ func TestSub_ExcludeFromSubTypes(t *testing.T) {
t.Fatalf("host excluded from clash must not appear in GetClash:\n%s", yaml)
}
}
// A host that forces plain TLS over a Reality inbound must not leave the
// Reality identity behind: pbk/sid/spx and the Reality dest sni describe a
// handshake the endpoint no longer performs.
func TestSub_HostTlsOverRealityDropsRealityParams(t *testing.T) {
seedSubDB(t)
reality := `{"network":"tcp","security":"reality","realitySettings":{"serverNames":["master-dest.example.com"],"publicKey":"MASTERPBK","shortIds":["ab12"],"fingerprint":"chrome"}}`
ib := seedSubInbound(t, "s1", "reality-in", 4461, 1, reality)
seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 1, Remark: "H", Address: "edge.example.com", Port: 443, Security: "tls"})
links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com")
if err != nil {
t.Fatalf("GetSubs: %v", err)
}
joined := strings.Join(links, "\n")
if !strings.Contains(joined, "security=tls") {
t.Fatalf("host forces tls, link must say so: %s", joined)
}
for _, leaked := range []string{"pbk=", "sid=", "spx=", "sni=master-dest.example.com"} {
if strings.Contains(joined, leaked) {
t.Fatalf("reality parameter %q survived a tls host override: %s", leaked, joined)
}
}
}