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
+46
View File
@@ -59,6 +59,29 @@ export const EXAMPLES: Record<string, unknown> = {
"subEnableRouting": false,
"subEncrypt": false,
"subExpiredTemplate": "",
"subHappAlwaysHwid": false,
"subHappAutoConnect": false,
"subHappAutoConnectType": "",
"subHappAutoDetect": false,
"subHappColorProfile": "",
"subHappExcludeApns": false,
"subHappExcludeRoutes": "",
"subHappFallbackUrl": "",
"subHappNewUrl": "",
"subHappNoLimit": false,
"subHappNotificationExpire": false,
"subHappPerAppList": "",
"subHappPerAppMode": "",
"subHappPingType": "",
"subHappProviderId": "",
"subHappSubExpire": false,
"subHappSubExpireButtonLink": "",
"subHappSubInfoButtonLink": "",
"subHappSubInfoButtonText": "",
"subHappSubInfoColor": "",
"subHappSubInfoText": "",
"subHappTunMode": "",
"subHappTunType": "",
"subHideSettings": false,
"subIncyEnableRouting": false,
"subIncyRoutingRules": "",
@@ -176,6 +199,29 @@ export const EXAMPLES: Record<string, unknown> = {
"subEnableRouting": false,
"subEncrypt": false,
"subExpiredTemplate": "",
"subHappAlwaysHwid": false,
"subHappAutoConnect": false,
"subHappAutoConnectType": "",
"subHappAutoDetect": false,
"subHappColorProfile": "",
"subHappExcludeApns": false,
"subHappExcludeRoutes": "",
"subHappFallbackUrl": "",
"subHappNewUrl": "",
"subHappNoLimit": false,
"subHappNotificationExpire": false,
"subHappPerAppList": "",
"subHappPerAppMode": "",
"subHappPingType": "",
"subHappProviderId": "",
"subHappSubExpire": false,
"subHappSubExpireButtonLink": "",
"subHappSubInfoButtonLink": "",
"subHappSubInfoButtonText": "",
"subHappSubInfoColor": "",
"subHappSubInfoText": "",
"subHappTunMode": "",
"subHappTunType": "",
"subHideSettings": false,
"subIncyEnableRouting": false,
"subIncyRoutingRules": "",
+186
View File
@@ -194,6 +194,76 @@ export const SCHEMAS: Record<string, unknown> = {
"subExpiredTemplate": {
"type": "string"
},
"subHappAlwaysHwid": {
"type": "boolean"
},
"subHappAutoConnect": {
"type": "boolean"
},
"subHappAutoConnectType": {
"type": "string"
},
"subHappAutoDetect": {
"description": "Happ client customization settings (app-management / routing / UX).",
"type": "boolean"
},
"subHappColorProfile": {
"type": "string"
},
"subHappExcludeApns": {
"type": "boolean"
},
"subHappExcludeRoutes": {
"type": "string"
},
"subHappFallbackUrl": {
"type": "string"
},
"subHappNewUrl": {
"type": "string"
},
"subHappNoLimit": {
"type": "boolean"
},
"subHappNotificationExpire": {
"type": "boolean"
},
"subHappPerAppList": {
"type": "string"
},
"subHappPerAppMode": {
"type": "string"
},
"subHappPingType": {
"type": "string"
},
"subHappProviderId": {
"type": "string"
},
"subHappSubExpire": {
"type": "boolean"
},
"subHappSubExpireButtonLink": {
"type": "string"
},
"subHappSubInfoButtonLink": {
"type": "string"
},
"subHappSubInfoButtonText": {
"type": "string"
},
"subHappSubInfoColor": {
"type": "string"
},
"subHappSubInfoText": {
"type": "string"
},
"subHappTunMode": {
"type": "string"
},
"subHappTunType": {
"type": "string"
},
"subHideSettings": {
"type": "boolean"
},
@@ -417,6 +487,29 @@ export const SCHEMAS: Record<string, unknown> = {
"subEnableRouting",
"subEncrypt",
"subExpiredTemplate",
"subHappAlwaysHwid",
"subHappAutoConnect",
"subHappAutoConnectType",
"subHappAutoDetect",
"subHappColorProfile",
"subHappExcludeApns",
"subHappExcludeRoutes",
"subHappFallbackUrl",
"subHappNewUrl",
"subHappNoLimit",
"subHappNotificationExpire",
"subHappPerAppList",
"subHappPerAppMode",
"subHappPingType",
"subHappProviderId",
"subHappSubExpire",
"subHappSubExpireButtonLink",
"subHappSubInfoButtonLink",
"subHappSubInfoButtonText",
"subHappSubInfoColor",
"subHappSubInfoText",
"subHappTunMode",
"subHappTunType",
"subHideSettings",
"subIncyEnableRouting",
"subIncyRoutingRules",
@@ -685,6 +778,76 @@ export const SCHEMAS: Record<string, unknown> = {
"subExpiredTemplate": {
"type": "string"
},
"subHappAlwaysHwid": {
"type": "boolean"
},
"subHappAutoConnect": {
"type": "boolean"
},
"subHappAutoConnectType": {
"type": "string"
},
"subHappAutoDetect": {
"description": "Happ client customization settings (app-management / routing / UX).",
"type": "boolean"
},
"subHappColorProfile": {
"type": "string"
},
"subHappExcludeApns": {
"type": "boolean"
},
"subHappExcludeRoutes": {
"type": "string"
},
"subHappFallbackUrl": {
"type": "string"
},
"subHappNewUrl": {
"type": "string"
},
"subHappNoLimit": {
"type": "boolean"
},
"subHappNotificationExpire": {
"type": "boolean"
},
"subHappPerAppList": {
"type": "string"
},
"subHappPerAppMode": {
"type": "string"
},
"subHappPingType": {
"type": "string"
},
"subHappProviderId": {
"type": "string"
},
"subHappSubExpire": {
"type": "boolean"
},
"subHappSubExpireButtonLink": {
"type": "string"
},
"subHappSubInfoButtonLink": {
"type": "string"
},
"subHappSubInfoButtonText": {
"type": "string"
},
"subHappSubInfoColor": {
"type": "string"
},
"subHappSubInfoText": {
"type": "string"
},
"subHappTunMode": {
"type": "string"
},
"subHappTunType": {
"type": "string"
},
"subHideSettings": {
"type": "boolean"
},
@@ -915,6 +1078,29 @@ export const SCHEMAS: Record<string, unknown> = {
"subEnableRouting",
"subEncrypt",
"subExpiredTemplate",
"subHappAlwaysHwid",
"subHappAutoConnect",
"subHappAutoConnectType",
"subHappAutoDetect",
"subHappColorProfile",
"subHappExcludeApns",
"subHappExcludeRoutes",
"subHappFallbackUrl",
"subHappNewUrl",
"subHappNoLimit",
"subHappNotificationExpire",
"subHappPerAppList",
"subHappPerAppMode",
"subHappPingType",
"subHappProviderId",
"subHappSubExpire",
"subHappSubExpireButtonLink",
"subHappSubInfoButtonLink",
"subHappSubInfoButtonText",
"subHappSubInfoColor",
"subHappSubInfoText",
"subHappTunMode",
"subHappTunType",
"subHideSettings",
"subIncyEnableRouting",
"subIncyRoutingRules",
+46
View File
@@ -67,6 +67,29 @@ export interface AllSetting {
subEnableRouting: boolean;
subEncrypt: boolean;
subExpiredTemplate: string;
subHappAlwaysHwid: boolean;
subHappAutoConnect: boolean;
subHappAutoConnectType: string;
subHappAutoDetect: boolean;
subHappColorProfile: string;
subHappExcludeApns: boolean;
subHappExcludeRoutes: string;
subHappFallbackUrl: string;
subHappNewUrl: string;
subHappNoLimit: boolean;
subHappNotificationExpire: boolean;
subHappPerAppList: string;
subHappPerAppMode: string;
subHappPingType: string;
subHappProviderId: string;
subHappSubExpire: boolean;
subHappSubExpireButtonLink: string;
subHappSubInfoButtonLink: string;
subHappSubInfoButtonText: string;
subHappSubInfoColor: string;
subHappSubInfoText: string;
subHappTunMode: string;
subHappTunType: string;
subHideSettings: boolean;
subIncyEnableRouting: boolean;
subIncyRoutingRules: string;
@@ -185,6 +208,29 @@ export interface AllSettingView {
subEnableRouting: boolean;
subEncrypt: boolean;
subExpiredTemplate: string;
subHappAlwaysHwid: boolean;
subHappAutoConnect: boolean;
subHappAutoConnectType: string;
subHappAutoDetect: boolean;
subHappColorProfile: string;
subHappExcludeApns: boolean;
subHappExcludeRoutes: string;
subHappFallbackUrl: string;
subHappNewUrl: string;
subHappNoLimit: boolean;
subHappNotificationExpire: boolean;
subHappPerAppList: string;
subHappPerAppMode: string;
subHappPingType: string;
subHappProviderId: string;
subHappSubExpire: boolean;
subHappSubExpireButtonLink: string;
subHappSubInfoButtonLink: string;
subHappSubInfoButtonText: string;
subHappSubInfoColor: string;
subHappSubInfoText: string;
subHappTunMode: string;
subHappTunType: string;
subHideSettings: boolean;
subIncyEnableRouting: boolean;
subIncyRoutingRules: string;
+46
View File
@@ -83,6 +83,29 @@ export const AllSettingSchema = z.object({
subEnableRouting: z.boolean(),
subEncrypt: z.boolean(),
subExpiredTemplate: z.string(),
subHappAlwaysHwid: z.boolean(),
subHappAutoConnect: z.boolean(),
subHappAutoConnectType: z.string(),
subHappAutoDetect: z.boolean(),
subHappColorProfile: z.string(),
subHappExcludeApns: z.boolean(),
subHappExcludeRoutes: z.string(),
subHappFallbackUrl: z.string(),
subHappNewUrl: z.string(),
subHappNoLimit: z.boolean(),
subHappNotificationExpire: z.boolean(),
subHappPerAppList: z.string(),
subHappPerAppMode: z.string(),
subHappPingType: z.string(),
subHappProviderId: z.string(),
subHappSubExpire: z.boolean(),
subHappSubExpireButtonLink: z.string(),
subHappSubInfoButtonLink: z.string(),
subHappSubInfoButtonText: z.string(),
subHappSubInfoColor: z.string(),
subHappSubInfoText: z.string(),
subHappTunMode: z.string(),
subHappTunType: z.string(),
subHideSettings: z.boolean(),
subIncyEnableRouting: z.boolean(),
subIncyRoutingRules: z.string(),
@@ -202,6 +225,29 @@ export const AllSettingViewSchema = z.object({
subEnableRouting: z.boolean(),
subEncrypt: z.boolean(),
subExpiredTemplate: z.string(),
subHappAlwaysHwid: z.boolean(),
subHappAutoConnect: z.boolean(),
subHappAutoConnectType: z.string(),
subHappAutoDetect: z.boolean(),
subHappColorProfile: z.string(),
subHappExcludeApns: z.boolean(),
subHappExcludeRoutes: z.string(),
subHappFallbackUrl: z.string(),
subHappNewUrl: z.string(),
subHappNoLimit: z.boolean(),
subHappNotificationExpire: z.boolean(),
subHappPerAppList: z.string(),
subHappPerAppMode: z.string(),
subHappPingType: z.string(),
subHappProviderId: z.string(),
subHappSubExpire: z.boolean(),
subHappSubExpireButtonLink: z.string(),
subHappSubInfoButtonLink: z.string(),
subHappSubInfoButtonText: z.string(),
subHappSubInfoColor: z.string(),
subHappSubInfoText: z.string(),
subHappTunMode: z.string(),
subHappTunType: z.string(),
subHideSettings: z.boolean(),
subIncyEnableRouting: z.boolean(),
subIncyRoutingRules: z.string(),