fix(sub): append host serverDescription to hysteria links (#6740)

* fix(sub): append host serverDescription to hysteria links

A host's Description reached vless/trojan/ss through buildEndpointLinks, but
genHysteriaLink renders the fragment in its own externalProxy loop and never
added the suffix, so Happ fell back to its "Hysteria | hysteria | TLS" caption
for every Hysteria server on a host that also serves VLESS (#6738).

Reuse appendHappServerDescription with the description the endpoint map already
carries, so no key lookup is duplicated and a host with no description emits the
same bytes as before. genTuicLink (service.go:912) has the same gap; left alone
to keep this diff to the reported protocol.

Regression test is red without the fix for both hysteria:// and hysteria2://.

* chore(sub): trim the hysteria serverDescription regression test

The no-description test passed with and without the #6738 fix: the empty
description branch is already pinned by TestAppendHappServerDescription, so
it certified nothing about this change. Also cut the remaining test's comment
block to the two-line limit CLAUDE.md sets.

---------

Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
冰
2026-10-06 01:05:13 +08:00
committed by GitHub
parent b42a1c0ba1
commit e897b0957a
2 changed files with 62 additions and 1 deletions
@@ -0,0 +1,57 @@
package sub
import (
"encoding/json"
"strings"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
// #6738: without the host's Description on hysteria(2):// links, Happ falls back
// to its own "Hysteria | hysteria | TLS" caption on a host that also serves VLESS.
func TestGenHysteriaLinkAppendsHostServerDescription(t *testing.T) {
tests := map[string]struct {
version int
scheme string
}{
"hysteria v1": {version: 1, scheme: "hysteria://"},
"hysteria v2": {version: 2, scheme: "hysteria2://"},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
host := &model.Host{
Address: "hy.example.com", Port: 443,
Remark: "Poland", ServerDescription: "Wi-Fi",
}
stream := map[string]any{
"security": "tls",
"externalProxy": []any{hostToExternalProxyMap(host, "hy.example.com", 443)},
}
rawStream, err := json.Marshal(stream)
if err != nil {
t.Fatalf("marshal stream settings: %v", err)
}
// The inbound's own `version` picks the hysteria vs hysteria2 scheme.
rawSettings, err := json.Marshal(map[string]any{
"version": tc.version,
"clients": []any{map[string]any{"auth": "secret", "email": "user"}},
})
if err != nil {
t.Fatalf("marshal inbound settings: %v", err)
}
in := &model.Inbound{
Id: 920010, Listen: "203.0.113.1", Port: 443, Protocol: model.Hysteria,
Remark: "hy", StreamSettings: string(rawStream), Settings: string(rawSettings),
}
got := (&SubService{}).genHysteriaLink(in, "user")
if !strings.HasPrefix(got, tc.scheme) {
t.Fatalf("link scheme changed.\n got: %s\nwant prefix: %s", got, tc.scheme)
}
// base64("Wi-Fi"), matching the reporter's subscription output.
if !strings.HasSuffix(got, "?serverDescription=V2ktRmk=") {
t.Fatalf("host serverDescription missing from fragment.\n got: %s\nwant suffix: ?serverDescription=V2ktRmk=", got)
}
})
}
}
+5 -1
View File
@@ -1535,7 +1535,11 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin
applyExternalProxyHysteriaParams(ep, epParams)
link := fmt.Sprintf("%s://%s@%s", protocol, auth, joinHostPort(dest, int(portF)))
links = append(links, buildLinkWithParams(link, epParams, s.endpointRemark(inbound, email, ep, "quic")))
// 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)
links = append(links, buildLinkWithParams(link, epParams, remark))
}
return strings.Join(links, "\n")
}