Ver Fonte

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
Sanaei há 9 horas atrás
pai
commit
4e423fa452
2 ficheiros alterados com 38 adições e 0 exclusões
  1. 14 0
      internal/sub/endpoint.go
  2. 24 0
      internal/sub/host_sub_test.go

+ 14 - 0
internal/sub/endpoint.go

@@ -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 - 0
internal/sub/host_sub_test.go

@@ -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)
+		}
+	}
+}