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")