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
+125
View File
@@ -102,6 +102,29 @@ var defaultValueMap = map[string]string{
"subEnableRouting": "false",
"subRoutingRules": "",
"subHideSettings": "false",
"subHappAutoDetect": "false",
"subHappProviderId": "",
"subHappNewUrl": "",
"subHappFallbackUrl": "",
"subHappSubInfoColor": "blue",
"subHappSubInfoText": "",
"subHappSubInfoButtonText": "",
"subHappSubInfoButtonLink": "",
"subHappSubExpire": "false",
"subHappSubExpireButtonLink": "",
"subHappNotificationExpire": "false",
"subHappNoLimit": "false",
"subHappAlwaysHwid": "false",
"subHappTunMode": "",
"subHappTunType": "",
"subHappExcludeRoutes": "",
"subHappExcludeApns": "false",
"subHappColorProfile": "",
"subHappPingType": "",
"subHappAutoConnect": "false",
"subHappAutoConnectType": "lowestdelay",
"subHappPerAppMode": "off",
"subHappPerAppList": "",
"subIncyEnableRouting": "false",
"subIncyRoutingRules": "",
"subListen": "",
@@ -820,6 +843,98 @@ func (s *SettingService) GetSubHideSettings() (bool, error) {
return s.getBool("subHideSettings")
}
func (s *SettingService) GetSubHappAutoDetect() (bool, error) {
return s.getBool("subHappAutoDetect")
}
func (s *SettingService) GetSubHappProviderId() (string, error) {
return s.getString("subHappProviderId")
}
func (s *SettingService) GetSubHappNewUrl() (string, error) {
return s.getString("subHappNewUrl")
}
func (s *SettingService) GetSubHappFallbackUrl() (string, error) {
return s.getString("subHappFallbackUrl")
}
func (s *SettingService) GetSubHappSubInfoColor() (string, error) {
return s.getString("subHappSubInfoColor")
}
func (s *SettingService) GetSubHappSubInfoText() (string, error) {
return s.getString("subHappSubInfoText")
}
func (s *SettingService) GetSubHappSubInfoButtonText() (string, error) {
return s.getString("subHappSubInfoButtonText")
}
func (s *SettingService) GetSubHappSubInfoButtonLink() (string, error) {
return s.getString("subHappSubInfoButtonLink")
}
func (s *SettingService) GetSubHappSubExpire() (bool, error) {
return s.getBool("subHappSubExpire")
}
func (s *SettingService) GetSubHappSubExpireButtonLink() (string, error) {
return s.getString("subHappSubExpireButtonLink")
}
func (s *SettingService) GetSubHappNotificationExpire() (bool, error) {
return s.getBool("subHappNotificationExpire")
}
func (s *SettingService) GetSubHappNoLimit() (bool, error) {
return s.getBool("subHappNoLimit")
}
func (s *SettingService) GetSubHappAlwaysHwid() (bool, error) {
return s.getBool("subHappAlwaysHwid")
}
func (s *SettingService) GetSubHappTunMode() (string, error) {
return s.getString("subHappTunMode")
}
func (s *SettingService) GetSubHappTunType() (string, error) {
return s.getString("subHappTunType")
}
func (s *SettingService) GetSubHappExcludeRoutes() (string, error) {
return s.getString("subHappExcludeRoutes")
}
func (s *SettingService) GetSubHappExcludeApns() (bool, error) {
return s.getBool("subHappExcludeApns")
}
func (s *SettingService) GetSubHappColorProfile() (string, error) {
return s.getString("subHappColorProfile")
}
func (s *SettingService) GetSubHappPingType() (string, error) {
return s.getString("subHappPingType")
}
func (s *SettingService) GetSubHappAutoConnect() (bool, error) {
return s.getBool("subHappAutoConnect")
}
func (s *SettingService) GetSubHappAutoConnectType() (string, error) {
return s.getString("subHappAutoConnectType")
}
func (s *SettingService) GetSubHappPerAppMode() (string, error) {
return s.getString("subHappPerAppMode")
}
func (s *SettingService) GetSubHappPerAppList() (string, error) {
return s.getString("subHappPerAppList")
}
func (s *SettingService) GetSubIncyEnableRouting() (bool, error) {
return s.getBool("subIncyEnableRouting")
}
@@ -1363,6 +1478,16 @@ func validateSettingsURLs(allSetting *entity.AllSetting) error {
// the scheme instead of forcing SanitizeHTTPURL's http(s)-only rule.
allSetting.SubSupportUrl = common.EnsureURLScheme(allSetting.SubSupportUrl)
allSetting.SubProfileUrl = common.EnsureURLScheme(allSetting.SubProfileUrl)
for _, ptr := range []*string{
&allSetting.SubHappNewUrl,
&allSetting.SubHappFallbackUrl,
&allSetting.SubHappSubInfoButtonLink,
&allSetting.SubHappSubExpireButtonLink,
} {
if strings.TrimSpace(*ptr) != "" {
*ptr = common.EnsureURLScheme(strings.TrimSpace(*ptr))
}
}
for name, value := range map[string]*string{
"Happ routing source": &allSetting.SubRoutingRules,
"Clash/Mihomo routing source": &allSetting.SubClashRules,