feat(sub): add Happ client integration, routing presets, and app management (#6434)

* feat(sub): add Happ client integration, routing presets, and app management

Implement comprehensive Happ proxy client integration according to official developer specifications.

- Fix header emission on disabled routing and hidden settings to send explicit '0' headers rather than omitting, allowing Happ clients to reset cached settings.
- Add support for 'happ://routing/off' deeplink in routing validation.
- Preserve '?serverDescription=' query parameters in link fragments without escaping to support Happ server subtitles across VMess, VLESS, Trojan and SS.
- Add Happ application management headers: ProviderID, New-Url, Fallback-Url, Sub-Info banners, Sub-Expire notifications, No-Limit mode, hardware ID enforcement, TUN modes/types, route exclusions, APNS exclusions, and per-app proxy settings.
- Add curated routing presets (Iran Bypass, China Direct, AdBlock, Global) and interactive visual rule generator in frontend settings.
- Synchronize all 13 translation locales with native Persian, Russian, and Chinese translations.

* fix(sub): keep Happ header overrides behind the auto-detect opt-in

The Routing-Enable/Hide-Settings off values were emitted on the
User-Agent alone, so every panel that upgraded would push
"Routing-Enable: 0" — documented by happ.su as disabling routing
globally — to every Happ client without the operator enabling anything.
They now ride subHappAutoDetect like every other Happ header.

Two further mismatches against the vendor spec:

- serverDescription was written as a key of the VMess base64 JSON
  object. happ.su documents it as a "#Title?serverDescription=<base64>"
  link parameter or a JSON "meta" entry, so the caption never reached
  Happ while every other VMess consumer received an unknown key.
  Dropped rather than moved: emitting the documented form is unsafe
  here because our own parser base64-decodes the whole VMess body
  (internal/util/link/outbound.go).

- The TUN Mode dropdown stored the literal "default", forwarded as
  "Tun-Mode: default", where happ.su documents system|gvisor only. It
  now stores the unset value so no header is sent. TUN Type "default"
  is a documented value and is unchanged.

Each fix carries a test that fails without it.
This commit is contained in:
Pejman Yousefi
2026-09-10 17:16:12 +03:30
committed by GitHub
parent d5ab84e8d5
commit 1456658028
35 changed files with 3460 additions and 51 deletions
+26
View File
@@ -116,6 +116,32 @@ func TestBuildEndpointVmessLinks(t *testing.T) {
}
}
// happ.su documents serverDescription as a "#title?serverDescription=<base64>"
// link parameter, never a key of the VMess object, so nothing may leak into it.
func TestBuildEndpointVmessLinks_HostServerDescription(t *testing.T) {
s := &SubService{}
in := &model.Inbound{Remark: "ib"}
baseObj := map[string]any{"v": "2", "add": "base.example.com", "port": 443, "type": "none", "id": "uid", "scy": "auto", "net": "tcp", "tls": "none"}
host := &model.Host{Address: "a.example.com", Port: 8443, ServerDescription: "Berlin premium"}
eps := []ShareEndpoint{externalProxyToEndpoint(hostToExternalProxyMap(host, "a.example.com", 8443))}
got := s.buildEndpointVmessLinks(eps, baseObj, in, "user", "tcp")
raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(got, "vmess://"))
if err != nil {
t.Fatalf("decode vmess link: %v", err)
}
var obj map[string]any
if err := json.Unmarshal(raw, &obj); err != nil {
t.Fatalf("unmarshal vmess object: %v", err)
}
if obj["add"] != "a.example.com" {
t.Fatalf("host endpoint not applied: add = %v", obj["add"])
}
if value, ok := obj["serverDescription"]; ok {
t.Fatalf("VMess object carries serverDescription = %v; it is not a VMess object key", value)
}
}
// N5 — a host's Final Mask is appended to the inbound's own fm param (#5831).
func TestBuildEndpointLinks_HostFinalMaskMerge(t *testing.T) {
s := &SubService{}