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
+16 -2
View File
@@ -52,6 +52,7 @@ type SUBController struct {
subEnableRouting bool
subRoutingRules string
subHideSettings bool
happConfig HappConfig
subIncyEnableRouting bool
subIncyRoutingRules string
@@ -110,6 +111,7 @@ type subControllerConfig struct {
subEnableRouting bool
subRoutingRules string
subHideSettings bool
happConfig HappConfig
subIncyEnableRouting bool
subIncyRoutingRules string
@@ -229,6 +231,10 @@ func WithSUBIncyRoutingRules(value string) SUBControllerOption {
return func(config *subControllerConfig) { config.subIncyRoutingRules = value }
}
func WithSUBHappConfig(value HappConfig) SUBControllerOption {
return func(config *subControllerConfig) { config.happConfig = value }
}
func defaultSUBControllerConfig() subControllerConfig {
return subControllerConfig{
subPath: "/sub/",
@@ -258,6 +264,7 @@ func NewSUBController(g *gin.RouterGroup, options ...SUBControllerOption) *SUBCo
subEnableRouting: config.subEnableRouting,
subRoutingRules: config.subRoutingRules,
subHideSettings: config.subHideSettings,
happConfig: config.happConfig,
subIncyEnableRouting: config.subIncyEnableRouting,
subIncyRoutingRules: config.subIncyRoutingRules,
@@ -845,16 +852,23 @@ func (a *SUBController) ApplyCommonHeaders(
c.Writer.Header().Set("Announce", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileAnnounce)))
}
// Advanced (Happ). Routing stays independent of the enable flag; remote
// values come only from the validated cache and never delay this response.
rules, remote, routingErr := resolveRoutingSource(remoteRoutingHapp, profileRoutingRules)
// The off values undo a previously pushed setting, so they ride the same
// opt-in as every other Happ header rather than reaching every Happ client.
happManaged := a.happConfig.AutoDetect && c.Request != nil && IsHappClient(c.GetHeader("User-Agent"))
if profileEnableRouting {
c.Writer.Header().Set("Routing-Enable", "true")
} else if happManaged {
c.Writer.Header().Set("Routing-Enable", "0")
}
if (routingErr == nil || !remote) && strings.TrimSpace(rules) != "" {
c.Writer.Header().Set("Routing", rules)
}
if profileHideSettings {
c.Writer.Header().Set("Hide-Settings", "1")
} else if happManaged {
c.Writer.Header().Set("Hide-Settings", "0")
}
ApplyHappHeaders(c, a.happConfig, happManaged)
}
+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{}
+141
View File
@@ -0,0 +1,141 @@
package sub
import (
"regexp"
"strings"
"github.com/gin-gonic/gin"
)
var happUserAgentRegex = regexp.MustCompile(`(?i)\bhapp\b`)
// HappConfig holds all Happ client customization parameters.
type HappConfig struct {
AutoDetect bool
ProviderId string
NewUrl string
FallbackUrl string
SubInfoColor string
SubInfoText string
SubInfoButtonText string
SubInfoButtonLink string
SubExpire bool
SubExpireButtonLink string
NotificationExpire bool
NoLimit bool
AlwaysHwid bool
TunMode string
TunType string
ExcludeRoutes string
ExcludeApns bool
ColorProfile string
PingType string
AutoConnect bool
AutoConnectType string
PerAppMode string
PerAppList string
}
// IsHappClient checks if the client user-agent identifies as Happ.
func IsHappClient(userAgent string) bool {
return happUserAgentRegex.MatchString(userAgent)
}
// ApplyHappHeaders sets standard and advanced Happ subscription headers.
func ApplyHappHeaders(c *gin.Context, cfg HappConfig, isHapp bool) {
if c == nil || c.Writer == nil || !cfg.AutoDetect || !isHapp {
return
}
if cfg.ProviderId != "" {
c.Writer.Header().Set("ProviderID", cfg.ProviderId)
}
if cfg.NewUrl != "" {
c.Writer.Header().Set("New-Url", cfg.NewUrl)
}
if cfg.FallbackUrl != "" {
c.Writer.Header().Set("Fallback-Url", cfg.FallbackUrl)
}
if text := strings.TrimSpace(cfg.SubInfoText); text != "" {
color := strings.TrimSpace(cfg.SubInfoColor)
switch strings.ToLower(color) {
case "primary", "info":
color = "blue"
case "success":
color = "green"
case "warning", "danger":
color = "red"
case "":
color = "blue"
}
c.Writer.Header().Set("Sub-Info-Color", color)
c.Writer.Header().Set("Sub-Info-Text", text)
if btnText := strings.TrimSpace(cfg.SubInfoButtonText); btnText != "" {
c.Writer.Header().Set("Sub-Info-Button-Text", btnText)
}
if btnLink := strings.TrimSpace(cfg.SubInfoButtonLink); btnLink != "" {
c.Writer.Header().Set("Sub-Info-Button-Link", btnLink)
}
}
if cfg.SubExpire {
c.Writer.Header().Set("Sub-Expire", "1")
if link := strings.TrimSpace(cfg.SubExpireButtonLink); link != "" {
c.Writer.Header().Set("Sub-Expire-Button-Link", link)
}
}
if cfg.NotificationExpire {
c.Writer.Header().Set("Notification-Subs-Expire", "1")
}
if cfg.NoLimit {
c.Writer.Header().Set("No-Limit-Enabled", "1")
}
if cfg.AlwaysHwid {
c.Writer.Header().Set("Subscription-Always-Hwid-Enable", "1")
}
if cfg.TunMode != "" {
c.Writer.Header().Set("Tun-Mode", cfg.TunMode)
}
if cfg.TunType != "" {
c.Writer.Header().Set("Tun-Type", cfg.TunType)
}
if routes := strings.TrimSpace(cfg.ExcludeRoutes); routes != "" {
c.Writer.Header().Set("Exclude-Routes", routes)
}
if cfg.ExcludeApns {
c.Writer.Header().Set("Exclude-Apns-Enable", "true")
}
if profile := strings.TrimSpace(cfg.ColorProfile); profile != "" {
profile = strings.ReplaceAll(strings.ReplaceAll(profile, "\r", ""), "\n", "")
c.Writer.Header().Set("Color-Profile", profile)
}
if ping := strings.TrimSpace(cfg.PingType); ping != "" {
if strings.EqualFold(ping, "http") {
ping = "proxy"
}
c.Writer.Header().Set("Ping-Type", ping)
}
if cfg.AutoConnect {
c.Writer.Header().Set("Subscription-Autoconnect", "1")
autoType := strings.TrimSpace(cfg.AutoConnectType)
switch strings.ToLower(autoType) {
case "fastest":
autoType = "lowestdelay"
case "last":
autoType = "lastused"
}
if autoType != "" {
c.Writer.Header().Set("Subscription-Autoconnect-Type", autoType)
}
}
if mode := strings.TrimSpace(cfg.PerAppMode); mode != "" && mode != "off" {
switch strings.ToLower(mode) {
case "include":
mode = "on"
case "exclude":
mode = "bypass"
}
c.Writer.Header().Set("Per-App-Proxy-Mode", mode)
if list := strings.TrimSpace(cfg.PerAppList); list != "" {
c.Writer.Header().Set("Per-App-Proxy-List", list)
}
}
}
+227
View File
@@ -0,0 +1,227 @@
package sub
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func TestNormalizeHappRouting_Off(t *testing.T) {
got, err := normalizeHappRouting([]byte("happ://routing/off"))
if err != nil {
t.Fatalf("normalizeHappRouting(off) error: %v", err)
}
if got != "happ://routing/off" {
t.Fatalf("normalizeHappRouting(off) = %q, want happ://routing/off", got)
}
}
func TestApplyCommonHeaders_HappClientHeaders(t *testing.T) {
gin.SetMode(gin.TestMode)
cfg := HappConfig{
AutoDetect: true,
ProviderId: "pid-test-123",
NewUrl: "https://new.example.com/sub",
FallbackUrl: "https://backup.example.com/sub",
SubInfoColor: "primary",
SubInfoText: "Welcome to VIP Network",
SubInfoButtonText: "Telegram",
SubInfoButtonLink: "https://t.me/example",
SubExpire: true,
SubExpireButtonLink: "https://renew.example.com",
NotificationExpire: true,
NoLimit: true,
AlwaysHwid: true,
TunMode: "gvisor",
TunType: "singbox",
ExcludeRoutes: "192.168.1.0/24, 10.0.0.0/8",
ExcludeApns: true,
ColorProfile: "{\"serverRowBackgroundColor\":\n\"#21003D67\"}",
PingType: "http",
AutoConnect: true,
AutoConnectType: "fastest",
PerAppMode: "include",
PerAppList: "com.google.chrome,com.meta.instagram",
}
controller := &SUBController{happConfig: cfg}
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(http.MethodGet, "/sub/test", nil)
ctx.Request.Header.Set("User-Agent", "Happ/1.2.0 (iPhone; iOS 17.5)")
controller.ApplyCommonHeaders(ctx, "upload=0; download=100; total=1000; expire=1800000000", "12", "MyTitle", "", "", "", false, "", false)
h := recorder.Header()
if h.Get("Routing-Enable") != "0" {
t.Fatalf("Routing-Enable = %q, want 0 for Happ with disabled routing", h.Get("Routing-Enable"))
}
if h.Get("Hide-Settings") != "0" {
t.Fatalf("Hide-Settings = %q, want 0 for Happ with disabled hideSettings", h.Get("Hide-Settings"))
}
if h.Get("ProviderID") != "pid-test-123" {
t.Fatalf("ProviderID = %q, want pid-test-123", h.Get("ProviderID"))
}
if h.Get("New-Url") != "https://new.example.com/sub" {
t.Fatalf("New-Url = %q", h.Get("New-Url"))
}
if h.Get("Fallback-Url") != "https://backup.example.com/sub" {
t.Fatalf("Fallback-Url = %q", h.Get("Fallback-Url"))
}
if h.Get("Sub-Info-Color") != "blue" || h.Get("Sub-Info-Text") != "Welcome to VIP Network" {
t.Fatalf("Sub-Info = %s / %s, want blue / Welcome to VIP Network", h.Get("Sub-Info-Color"), h.Get("Sub-Info-Text"))
}
if h.Get("Sub-Info-Button-Text") != "Telegram" || h.Get("Sub-Info-Button-Link") != "https://t.me/example" {
t.Fatalf("Sub-Info button = %s / %s", h.Get("Sub-Info-Button-Text"), h.Get("Sub-Info-Button-Link"))
}
if h.Get("Sub-Expire") != "1" || h.Get("Sub-Expire-Button-Link") != "https://renew.example.com" {
t.Fatalf("Sub-Expire = %s / %s", h.Get("Sub-Expire"), h.Get("Sub-Expire-Button-Link"))
}
if h.Get("Notification-Subs-Expire") != "1" {
t.Fatalf("Notification-Subs-Expire = %q", h.Get("Notification-Subs-Expire"))
}
if h.Get("No-Limit-Enabled") != "1" {
t.Fatalf("No-Limit-Enabled = %q", h.Get("No-Limit-Enabled"))
}
if h.Get("Subscription-Always-Hwid-Enable") != "1" {
t.Fatalf("Subscription-Always-Hwid-Enable = %q", h.Get("Subscription-Always-Hwid-Enable"))
}
if h.Get("Tun-Mode") != "gvisor" || h.Get("Tun-Type") != "singbox" {
t.Fatalf("Tun mode/type = %s / %s", h.Get("Tun-Mode"), h.Get("Tun-Type"))
}
if h.Get("Exclude-Routes") != "192.168.1.0/24, 10.0.0.0/8" || h.Get("Exclude-Apns-Enable") != "true" {
t.Fatalf("Exclude routes/apns = %s / %s", h.Get("Exclude-Routes"), h.Get("Exclude-Apns-Enable"))
}
if wantProfile := "{\"serverRowBackgroundColor\":\"#21003D67\"}"; h.Get("Color-Profile") != wantProfile {
t.Fatalf("Color-Profile = %q, want %q", h.Get("Color-Profile"), wantProfile)
}
if h.Get("Ping-Type") != "proxy" {
t.Fatalf("Ping-Type = %q, want proxy for http alias", h.Get("Ping-Type"))
}
if h.Get("Subscription-Autoconnect") != "1" || h.Get("Subscription-Autoconnect-Type") != "lowestdelay" {
t.Fatalf("Autoconnect = %s / %s, want 1 / lowestdelay for fastest alias", h.Get("Subscription-Autoconnect"), h.Get("Subscription-Autoconnect-Type"))
}
if h.Get("Per-App-Proxy-Mode") != "on" || h.Get("Per-App-Proxy-List") != "com.google.chrome,com.meta.instagram" {
t.Fatalf("Per-App = %s / %s, want on / com.google.chrome,com.meta.instagram", h.Get("Per-App-Proxy-Mode"), h.Get("Per-App-Proxy-List"))
}
}
func TestApplyHappHeaders_Gating(t *testing.T) {
gin.SetMode(gin.TestMode)
cfg := HappConfig{
AutoDetect: true,
ProviderId: "pid-secret",
SubInfoText: "Banner",
TunMode: "system",
}
t.Run("non-Happ User-Agent receives no headers", func(t *testing.T) {
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(http.MethodGet, "/sub/test", nil)
ctx.Request.Header.Set("User-Agent", "v2rayNG/1.8.5")
controller := &SUBController{happConfig: cfg}
controller.ApplyCommonHeaders(ctx, "", "", "Title", "", "", "", false, "", false)
if got := recorder.Header().Get("ProviderID"); got != "" {
t.Fatalf("ProviderID emitted to non-Happ client: %q", got)
}
if got := recorder.Header().Get("Sub-Info-Text"); got != "" {
t.Fatalf("Sub-Info-Text emitted to non-Happ client: %q", got)
}
if got := recorder.Header().Get("Tun-Mode"); got != "" {
t.Fatalf("Tun-Mode emitted to non-Happ client: %q", got)
}
})
t.Run("AutoDetect disabled suppresses Happ headers", func(t *testing.T) {
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(http.MethodGet, "/sub/test", nil)
ctx.Request.Header.Set("User-Agent", "Happ/1.2.0 (Android)")
disabledCfg := cfg
disabledCfg.AutoDetect = false
controller := &SUBController{happConfig: disabledCfg}
controller.ApplyCommonHeaders(ctx, "", "", "Title", "", "", "", false, "", false)
if got := recorder.Header().Get("ProviderID"); got != "" {
t.Fatalf("ProviderID emitted when AutoDetect is false: %q", got)
}
if got := recorder.Header().Get("Sub-Info-Text"); got != "" {
t.Fatalf("Sub-Info-Text emitted when AutoDetect is false: %q", got)
}
// happ.su documents routing-enable 0/false as "disables routing
// globally", so it must stay behind the same opt-in as the rest.
if got := recorder.Header().Get("Routing-Enable"); got != "" {
t.Fatalf("Routing-Enable emitted when AutoDetect is false: %q", got)
}
if got := recorder.Header().Get("Hide-Settings"); got != "" {
t.Fatalf("Hide-Settings emitted when AutoDetect is false: %q", got)
}
})
}
func TestApplyHappHeaders_Aliases(t *testing.T) {
gin.SetMode(gin.TestMode)
tests := []struct {
name string
cfg HappConfig
wantHeader string
wantValue string
}{
{
name: "color warning maps to red",
cfg: HappConfig{AutoDetect: true, SubInfoText: "Alert", SubInfoColor: "warning"},
wantHeader: "Sub-Info-Color",
wantValue: "red",
},
{
name: "color danger maps to red",
cfg: HappConfig{AutoDetect: true, SubInfoText: "Alert", SubInfoColor: "danger"},
wantHeader: "Sub-Info-Color",
wantValue: "red",
},
{
name: "color success maps to green",
cfg: HappConfig{AutoDetect: true, SubInfoText: "Ok", SubInfoColor: "success"},
wantHeader: "Sub-Info-Color",
wantValue: "green",
},
{
name: "autoconnect last maps to lastused",
cfg: HappConfig{AutoDetect: true, AutoConnect: true, AutoConnectType: "last"},
wantHeader: "Subscription-Autoconnect-Type",
wantValue: "lastused",
},
{
name: "per-app exclude maps to bypass",
cfg: HappConfig{AutoDetect: true, PerAppMode: "exclude", PerAppList: "app.id"},
wantHeader: "Per-App-Proxy-Mode",
wantValue: "bypass",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(http.MethodGet, "/sub/test", nil)
ctx.Request.Header.Set("User-Agent", "Happ/1.2.0 (iOS)")
controller := &SUBController{happConfig: tc.cfg}
controller.ApplyCommonHeaders(ctx, "", "", "Title", "", "", "", false, "", false)
if got := recorder.Header().Get(tc.wantHeader); got != tc.wantValue {
t.Fatalf("%s = %q, want %q", tc.wantHeader, got, tc.wantValue)
}
})
}
}
+3
View File
@@ -369,6 +369,9 @@ func normalizeHappRouting(body []byte) (string, error) {
}
return "happ://routing/onadd/" + base64.StdEncoding.EncodeToString(compact), nil
}
if text == "happ://routing/off" {
return text, nil
}
if strings.ContainsAny(text, "\r\n") {
return "", errors.New("Happ deeplink must be a single line")
}
+1 -2
View File
@@ -2166,8 +2166,7 @@ func appendQueryAndFragment(link string, params map[string]string, fragment, sec
if fragment != "" {
sb.WriteByte('#')
// Match the frontend's encodeURIComponent(remark): spaces become
// %20 (not + as in query strings).
// Match the frontend's encodeURIComponent(remark): spaces become %20.
sb.WriteString(strings.ReplaceAll(url.QueryEscape(fragment), "+", "%20"))
}
return sb.String()
+26
View File
@@ -215,6 +215,31 @@ func (s *Server) initRouter() (*gin.Engine, error) {
SubIncyRoutingRules = ""
}
happCfg := HappConfig{}
happCfg.AutoDetect, _ = s.settingService.GetSubHappAutoDetect()
happCfg.ProviderId, _ = s.settingService.GetSubHappProviderId()
happCfg.NewUrl, _ = s.settingService.GetSubHappNewUrl()
happCfg.FallbackUrl, _ = s.settingService.GetSubHappFallbackUrl()
happCfg.SubInfoColor, _ = s.settingService.GetSubHappSubInfoColor()
happCfg.SubInfoText, _ = s.settingService.GetSubHappSubInfoText()
happCfg.SubInfoButtonText, _ = s.settingService.GetSubHappSubInfoButtonText()
happCfg.SubInfoButtonLink, _ = s.settingService.GetSubHappSubInfoButtonLink()
happCfg.SubExpire, _ = s.settingService.GetSubHappSubExpire()
happCfg.SubExpireButtonLink, _ = s.settingService.GetSubHappSubExpireButtonLink()
happCfg.NotificationExpire, _ = s.settingService.GetSubHappNotificationExpire()
happCfg.NoLimit, _ = s.settingService.GetSubHappNoLimit()
happCfg.AlwaysHwid, _ = s.settingService.GetSubHappAlwaysHwid()
happCfg.TunMode, _ = s.settingService.GetSubHappTunMode()
happCfg.TunType, _ = s.settingService.GetSubHappTunType()
happCfg.ExcludeRoutes, _ = s.settingService.GetSubHappExcludeRoutes()
happCfg.ExcludeApns, _ = s.settingService.GetSubHappExcludeApns()
happCfg.ColorProfile, _ = s.settingService.GetSubHappColorProfile()
happCfg.PingType, _ = s.settingService.GetSubHappPingType()
happCfg.AutoConnect, _ = s.settingService.GetSubHappAutoConnect()
happCfg.AutoConnectType, _ = s.settingService.GetSubHappAutoConnectType()
happCfg.PerAppMode, _ = s.settingService.GetSubHappPerAppMode()
happCfg.PerAppList, _ = s.settingService.GetSubHappPerAppList()
// set per-request localizer from headers/cookies
engine.Use(locale.LocalizerMiddleware())
@@ -296,6 +321,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
WithSUBEnableRouting(SubEnableRouting),
WithSUBRoutingRules(SubRoutingRules),
WithSUBHideSettings(SubHideSettings),
WithSUBHappConfig(happCfg),
WithSUBIncyEnableRouting(SubIncyEnableRouting),
WithSUBIncyRoutingRules(SubIncyRoutingRules),
)