From 243b5c9da78884066fa8d50bbace36803dd46baa Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Fri, 13 Nov 2026 04:54:13 +0530 Subject: [PATCH] fix(sub): send the serverDescription tail only to clients that read it (#7108) A Host Description is appended to every raw-subscription node name as a base64 `?serverDescription=` tail. Only Happ and INCY read it; v2rayN, v2rayNG, OpenClash and the rest show it as part of the node name, and the only way out was clearing the Description (#7105). The raw subscription body now carries the tail only when the requesting User-Agent is Happ or INCY, for the endpoint links and the Hysteria2 loop alike. Copied links, QR codes and the panel's link export keep it, since the server cannot tell which client they end up in. Closes #7105 --- internal/sub/controller.go | 1 + internal/sub/controller_test.go | 26 +++++++++++++++++++ internal/sub/endpoint.go | 2 +- .../sub/hysteria_server_description_test.go | 3 +++ internal/sub/service.go | 7 ++++- 5 files changed, 37 insertions(+), 2 deletions(-) diff --git a/internal/sub/controller.go b/internal/sub/controller.go index e2de1f750..56d85d2b8 100644 --- a/internal/sub/controller.go +++ b/internal/sub/controller.go @@ -502,6 +502,7 @@ func (a *SUBController) subs(c *gin.Context) { scheme, host, hostWithPort, _ := a.subService.ResolveRequest(c) subReq := a.subService.ForRequest(host) subReq.subscriptionBody = true + subReq.omitServerDescription = !IsHappClient(userAgent) && !IsIncyClient(userAgent) subs, _, _, traffic, err := subReq.getSubs(subId) if err != nil || subs == nil { writeSubError(c, err) diff --git a/internal/sub/controller_test.go b/internal/sub/controller_test.go index fb119c8a9..c07acc016 100644 --- a/internal/sub/controller_test.go +++ b/internal/sub/controller_test.go @@ -803,6 +803,32 @@ func TestLegacyClashEndpointNormalizesShadowsocksCipher(t *testing.T) { } } +// #7105: only Happ and INCY read the tail; other clients show it in the node name. +func TestRawSubServerDescriptionByClient(t *testing.T) { + seedSubDB(t) + seedSubInbound(t, "s1", "vl", 4492, 1, `{"network":"tcp","security":"none","externalProxy":[{"forceTls":"same","dest":"a.example.com","port":443,"remark":"A","serverDescription":"Berlin"}]}`) + gin.SetMode(gin.TestMode) + + cases := map[string]bool{"Happ/3.1": true, "INCY/1.0.0/ios": true, "v2rayN/7.10": false} + for userAgent, wantTail := range cases { + t.Run(userAgent, func(t *testing.T) { + router := gin.New() + NewSUBController(router.Group("/"), WithSUBEncryption(false)) + req := httptest.NewRequest(http.MethodGet, "http://sub.example.com/sub/s1", nil) + req.Header.Set("User-Agent", userAgent) + resp := httptest.NewRecorder() + router.ServeHTTP(resp, req) + body := resp.Body.String() + if resp.Code != http.StatusOK || !strings.Contains(body, "vless") { + t.Fatalf("status=%d, body=%s", resp.Code, body) + } + if got := strings.Contains(body, "?serverDescription=QmVybGlu"); got != wantTail { + t.Fatalf("tail present = %v, want %v; body=%s", got, wantTail, body) + } + }) + } +} + func TestIncyRoutingIsolatedFromHappHeaders(t *testing.T) { seedSubDB(t) seedSubInbound(t, "s1", "vl", 4492, 1, `{"network":"tcp","security":"none"}`) diff --git a/internal/sub/endpoint.go b/internal/sub/endpoint.go index 5d8c9d34d..0db4dd5fb 100644 --- a/internal/sub/endpoint.go +++ b/internal/sub/endpoint.go @@ -136,7 +136,7 @@ func (s *SubService) buildEndpointLinks( applyEndpointFinalMask(e, nextParams) applyEndpointAllowInsecure(e, nextParams, securityToApply) remark := makeRemark(e) - if e.ServerDescription != "" { + if e.ServerDescription != "" && !s.omitServerDescription { remark = appendHappServerDescription(remark, e.ServerDescription) } links = append(links, buildLinkWithParamsAndSecurity( diff --git a/internal/sub/hysteria_server_description_test.go b/internal/sub/hysteria_server_description_test.go index 2dd7c440e..9f750a2c5 100644 --- a/internal/sub/hysteria_server_description_test.go +++ b/internal/sub/hysteria_server_description_test.go @@ -52,6 +52,9 @@ func TestGenHysteriaLinkAppendsHostServerDescription(t *testing.T) { if !strings.HasSuffix(got, "?serverDescription=V2ktRmk=") { t.Fatalf("host serverDescription missing from fragment.\n got: %s\nwant suffix: ?serverDescription=V2ktRmk=", got) } + if got := (&SubService{omitServerDescription: true}).genHysteriaLink(in, "user"); strings.Contains(got, "serverDescription") { + t.Fatalf("tail not omitted: %s", got) + } }) } } diff --git a/internal/sub/service.go b/internal/sub/service.go index 49c53cef4..4287f28d2 100644 --- a/internal/sub/service.go +++ b/internal/sub/service.go @@ -46,6 +46,9 @@ type SubService struct { // other context — the sub info page, the panel's link/QR displays — renders // the name-only template, like Remnawave. subscriptionBody bool + // omitServerDescription drops the ?serverDescription= tail, which only Happ + // and INCY read; other clients show it in the node name (#7105). + omitServerDescription bool // usageShown emits info once per subscription identity, including twins. // PrepareForRequest resets this per-request state. usageShown map[string]bool @@ -1744,7 +1747,9 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin // VLESS/Trojan/SS get the host's description through buildEndpointLinks; // this loop renders the fragment itself, so add it here too (#6738). remark := s.endpointRemark(inbound, email, ep, "quic") - remark = appendHappServerDescription(remark, externalProxyToEndpoint(ep).ServerDescription) + if !s.omitServerDescription { + remark = appendHappServerDescription(remark, externalProxyToEndpoint(ep).ServerDescription) + } links = append(links, buildLinkWithParams(link, epParams, remark)) } return strings.Join(links, "\n")