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
@@ -0,0 +1,637 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Input, Modal, Select, Space, Switch, Tabs, message } from 'antd';
import {
BranchesOutlined,
BuildOutlined,
CloudSyncOutlined,
DesktopOutlined,
MobileOutlined,
NotificationOutlined,
ThunderboltOutlined,
} from '@ant-design/icons';
import type { AllSetting } from '@/models/setting';
import { SettingListItem } from '@/components/ui';
import { buildHappPresetDeeplink, parseList, toBase64Utf8 } from './happPresets';
interface HappSettingsContentProps {
allSetting: AllSetting;
updateSetting: (patch: Partial<AllSetting>) => void;
isMobile: boolean;
remoteSourceBadge: (val: string) => React.ReactNode;
}
export default function HappSettingsContent({
allSetting,
updateSetting,
isMobile,
remoteSourceBadge,
}: HappSettingsContentProps) {
const { t } = useTranslation();
const [selectedPreset, setSelectedPreset] = useState<string>('iran-bypass');
const [isModalOpen, setIsModalOpen] = useState(false);
const [directDomains, setDirectDomains] = useState('');
const [proxyDomains, setProxyDomains] = useState('');
const [blockDomains, setBlockDomains] = useState('');
const [directIPs, setDirectIPs] = useState('');
const [proxyIPs, setProxyIPs] = useState('');
const [blockIPs, setBlockIPs] = useState('');
const applyPreset = () => {
const payload = buildHappPresetDeeplink(selectedPreset);
if (payload) {
updateSetting({ subRoutingRules: payload });
message.success(t('pages.settings.subHappPresetApplied'));
}
};
const handleBuildDeeplink = () => {
interface FieldRule {
type: string;
outboundTag: string;
domain?: string[];
ip?: string[];
network?: string;
}
const rules: FieldRule[] = [];
const bDom = parseList(blockDomains);
const bIp = parseList(blockIPs);
if (bDom.length > 0 || bIp.length > 0) {
rules.push({
type: 'field',
outboundTag: 'block',
...(bDom.length > 0 ? { domain: bDom } : {}),
...(bIp.length > 0 ? { ip: bIp } : {}),
});
}
const dDom = parseList(directDomains);
const dIp = parseList(directIPs);
if (dDom.length > 0 || dIp.length > 0) {
rules.push({
type: 'field',
outboundTag: 'direct',
...(dDom.length > 0 ? { domain: dDom } : {}),
...(dIp.length > 0 ? { ip: dIp } : {}),
});
}
const pDom = parseList(proxyDomains);
const pIp = parseList(proxyIPs);
if (pDom.length > 0 || pIp.length > 0) {
rules.push({
type: 'field',
outboundTag: 'proxy',
...(pDom.length > 0 ? { domain: pDom } : {}),
...(pIp.length > 0 ? { ip: pIp } : {}),
});
}
rules.push({
type: 'field',
outboundTag: 'proxy',
network: 'tcp,udp',
});
const deeplink = 'happ://routing/onadd/' + toBase64Utf8(JSON.stringify({ rules }));
updateSetting({ subRoutingRules: deeplink });
setIsModalOpen(false);
message.success(t('pages.settings.subHappDeeplinkGenerated'));
};
return (
<>
<Tabs
type="card"
size="small"
items={[
{
key: 'routing',
label: (
<span>
<BranchesOutlined /> {!isMobile && t('pages.settings.subHappGroupRouting')}
</span>
),
children: (
<>
<SettingListItem
paddings="small"
title={t('pages.settings.subEnableRouting')}
description={t('pages.settings.subEnableRoutingDesc')}
>
<Switch
checked={allSetting.subEnableRouting}
onChange={(v) => updateSetting({ subEnableRouting: v })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappPresets')}
description={t('pages.settings.subHappPresetsDesc')}
>
<Space orientation="horizontal" style={{ width: '100%' }}>
<Select
value={selectedPreset}
style={{ minWidth: 170 }}
onChange={setSelectedPreset}
options={[
{ value: 'iran-bypass', label: t('pages.settings.subHappPresetIran') },
{ value: 'china-direct', label: t('pages.settings.subHappPresetChina') },
{ value: 'adblock', label: t('pages.settings.subHappPresetAdblock') },
{ value: 'global', label: t('pages.settings.subHappPresetGlobal') },
{ value: 'off', label: t('pages.settings.subHappPresetOff') },
]}
/>
<Button type="primary" onClick={applyPreset}>
{t('pages.settings.subHappPresets')}
</Button>
</Space>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappVisualBuilder')}
description={t('pages.settings.subHappVisualBuilderDesc')}
>
<Button icon={<BuildOutlined />} onClick={() => setIsModalOpen(true)}>
{t('pages.settings.subHappVisualBuilder')}
</Button>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subRoutingRules')}
badge={remoteSourceBadge(allSetting.subRoutingRules)}
description={t('pages.settings.subRoutingRulesDesc')}
>
<Input.TextArea
value={allSetting.subRoutingRules}
rows={4}
placeholder="happ://routing/onadd/... or https://.../DEFAULT.DEEPLINK"
onChange={(e) => updateSetting({ subRoutingRules: e.target.value })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappNoLimit')}
description={t('pages.settings.subHappNoLimitDesc')}
>
<Switch
checked={allSetting.subHappNoLimit}
onChange={(v) => updateSetting({ subHappNoLimit: v })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHideSettings')}
description={t('pages.settings.subHideSettingsDesc')}
>
<Switch
checked={allSetting.subHideSettings}
onChange={(v) => updateSetting({ subHideSettings: v })}
/>
</SettingListItem>
</>
),
},
{
key: 'banners',
label: (
<span>
<NotificationOutlined /> {!isMobile && t('pages.settings.subHappGroupBanners')}
</span>
),
children: (
<>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappSubInfoText')}
description={t('pages.settings.subHappSubInfoTextDesc')}
>
<Input
value={allSetting.subHappSubInfoText}
maxLength={200}
placeholder="Welcome to our high-speed network!"
onChange={(e) => updateSetting({ subHappSubInfoText: e.target.value })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappSubInfoColor')}
description={t('pages.settings.subHappSubInfoColorDesc')}
>
<Select
value={allSetting.subHappSubInfoColor || 'blue'}
style={{ width: '100%' }}
onChange={(v) => updateSetting({ subHappSubInfoColor: v })}
options={[
{ value: 'blue', label: t('pages.settings.subHappColorBlue') },
{ value: 'green', label: t('pages.settings.subHappColorGreen') },
{ value: 'red', label: t('pages.settings.subHappColorRed') },
]}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappSubInfoButtonText')}
description={t('pages.settings.subHappSubInfoButtonTextDesc')}
>
<Input
value={allSetting.subHappSubInfoButtonText}
maxLength={25}
placeholder="Support Channel"
onChange={(e) => updateSetting({ subHappSubInfoButtonText: e.target.value })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappSubInfoButtonLink')}
description={t('pages.settings.subHappSubInfoButtonLinkDesc')}
>
<Input
value={allSetting.subHappSubInfoButtonLink}
placeholder="https://t.me/your_channel"
onChange={(e) => updateSetting({ subHappSubInfoButtonLink: e.target.value })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappSubExpire')}
description={t('pages.settings.subHappSubExpireDesc')}
>
<Switch
checked={allSetting.subHappSubExpire}
onChange={(v) => updateSetting({ subHappSubExpire: v })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappSubExpireButtonLink')}
description={t('pages.settings.subHappSubExpireButtonLinkDesc')}
>
<Input
value={allSetting.subHappSubExpireButtonLink}
placeholder="https://example.com/renew"
onChange={(e) => updateSetting({ subHappSubExpireButtonLink: e.target.value })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappNotificationExpire')}
description={t('pages.settings.subHappNotificationExpireDesc')}
>
<Switch
checked={allSetting.subHappNotificationExpire}
onChange={(v) => updateSetting({ subHappNotificationExpire: v })}
/>
</SettingListItem>
</>
),
},
{
key: 'network',
label: (
<span>
<ThunderboltOutlined /> {!isMobile && t('pages.settings.subHappGroupNetwork')}
</span>
),
children: (
<>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappTunMode')}
description={t('pages.settings.subHappTunModeDesc')}
>
<Select
value={allSetting.subHappTunMode}
style={{ width: '100%' }}
onChange={(v) => updateSetting({ subHappTunMode: v })}
options={[
// happ.su documents tun-mode as system|gvisor only, so
// Default is the unset state rather than a third value.
{ value: '', label: t('pages.settings.subHappTunModeDefault') },
{ value: 'system', label: t('pages.settings.subHappTunModeSystem') },
{ value: 'gvisor', label: t('pages.settings.subHappTunModeGvisor') },
]}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappTunType')}
description={t('pages.settings.subHappTunTypeDesc')}
>
<Select
value={allSetting.subHappTunType || 'default'}
style={{ width: '100%' }}
onChange={(v) => updateSetting({ subHappTunType: v })}
options={[
{ value: 'singbox', label: t('pages.settings.subHappTunTypeSingbox') },
{ value: 'tun2proxy', label: t('pages.settings.subHappTunTypeTun2proxy') },
{ value: 'default', label: t('pages.settings.subHappTunTypeDefault') },
{ value: 'xray', label: t('pages.settings.subHappTunTypeXray') },
]}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappExcludeRoutes')}
description={t('pages.settings.subHappExcludeRoutesDesc')}
>
<Input
value={allSetting.subHappExcludeRoutes}
placeholder="192.168.0.0/16, 10.0.0.0/8"
onChange={(e) => updateSetting({ subHappExcludeRoutes: e.target.value })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappExcludeApns')}
description={t('pages.settings.subHappExcludeApnsDesc')}
>
<Switch
checked={allSetting.subHappExcludeApns}
onChange={(v) => updateSetting({ subHappExcludeApns: v })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappPingType')}
description={t('pages.settings.subHappPingTypeDesc')}
>
<Select
value={allSetting.subHappPingType || 'proxy'}
style={{ width: '100%' }}
onChange={(v) => updateSetting({ subHappPingType: v })}
options={[
{ value: 'proxy', label: t('pages.settings.subHappPingProxy') },
{ value: 'proxy-head', label: t('pages.settings.subHappPingProxyHead') },
{ value: 'tcp', label: t('pages.settings.subHappPingTcp') },
{ value: 'icmp', label: t('pages.settings.subHappPingIcmp') },
]}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappAutoConnect')}
description={t('pages.settings.subHappAutoConnectDesc')}
>
<Switch
checked={allSetting.subHappAutoConnect}
onChange={(v) => updateSetting({ subHappAutoConnect: v })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappAutoConnectType')}
description={t('pages.settings.subHappAutoConnectTypeDesc')}
>
<Select
value={allSetting.subHappAutoConnectType || 'lowestdelay'}
style={{ width: '100%' }}
onChange={(v) => updateSetting({ subHappAutoConnectType: v })}
options={[
{
value: 'lowestdelay',
label: t('pages.settings.subHappAutoConnectLowestDelay'),
},
{ value: 'lastused', label: t('pages.settings.subHappAutoConnectLastUsed') },
{ value: 'random', label: t('pages.settings.subHappAutoConnectRandom') },
]}
/>
</SettingListItem>
</>
),
},
{
key: 'themes',
label: (
<span>
<DesktopOutlined /> {!isMobile && t('pages.settings.subHappGroupThemes')}
</span>
),
children: (
<>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappColorProfile')}
description={t('pages.settings.subHappColorProfileDesc')}
>
<Input
value={allSetting.subHappColorProfile}
placeholder="default, violet, turquoise, cyberpunk, or custom JSON"
onChange={(e) => updateSetting({ subHappColorProfile: e.target.value })}
/>
</SettingListItem>
</>
),
},
{
key: 'failover',
label: (
<span>
<CloudSyncOutlined /> {!isMobile && t('pages.settings.subHappGroupFailover')}
</span>
),
children: (
<>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappAutoDetect')}
description={t('pages.settings.subHappAutoDetectDesc')}
>
<Switch
checked={allSetting.subHappAutoDetect}
onChange={(v) => updateSetting({ subHappAutoDetect: v })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappProviderId')}
description={t('pages.settings.subHappProviderIdDesc')}
>
<Input
value={allSetting.subHappProviderId}
placeholder="my-happ-provider"
onChange={(e) => updateSetting({ subHappProviderId: e.target.value })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappNewUrl')}
description={t('pages.settings.subHappNewUrlDesc')}
>
<Input
value={allSetting.subHappNewUrl}
placeholder="https://new-domain.com/sub/..."
onChange={(e) => updateSetting({ subHappNewUrl: e.target.value })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappFallbackUrl')}
description={t('pages.settings.subHappFallbackUrlDesc')}
>
<Input
value={allSetting.subHappFallbackUrl}
placeholder="https://backup-domain.com/sub/..."
onChange={(e) => updateSetting({ subHappFallbackUrl: e.target.value })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappAlwaysHwid')}
description={t('pages.settings.subHappAlwaysHwidDesc')}
>
<Switch
checked={allSetting.subHappAlwaysHwid}
onChange={(v) => updateSetting({ subHappAlwaysHwid: v })}
/>
</SettingListItem>
</>
),
},
{
key: 'android',
label: (
<span>
<MobileOutlined /> {!isMobile && t('pages.settings.subHappGroupAndroid')}
</span>
),
children: (
<>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappPerAppMode')}
description={t('pages.settings.subHappPerAppModeDesc')}
>
<Select
value={allSetting.subHappPerAppMode || 'off'}
style={{ width: '100%' }}
onChange={(v) => updateSetting({ subHappPerAppMode: v })}
options={[
{ value: 'off', label: t('pages.settings.subHappPerAppOff') },
{ value: 'on', label: t('pages.settings.subHappPerAppOn') },
{ value: 'bypass', label: t('pages.settings.subHappPerAppBypass') },
]}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappPerAppList')}
description={t('pages.settings.subHappPerAppListDesc')}
>
<Input.TextArea
value={allSetting.subHappPerAppList}
rows={4}
placeholder="org.telegram.messenger, com.google.android.youtube"
onChange={(e) => updateSetting({ subHappPerAppList: e.target.value })}
/>
</SettingListItem>
</>
),
},
]}
/>
<Modal
title={t('pages.settings.subHappModalTitle')}
open={isModalOpen}
onCancel={() => setIsModalOpen(false)}
onOk={handleBuildDeeplink}
okText={t('pages.settings.subHappBuildDeeplink')}
width={650}
>
<Space direction="vertical" style={{ width: '100%', marginTop: 12 }} size="middle">
<div>
<div style={{ fontWeight: 600, marginBottom: 4 }}>
{t('pages.settings.subHappDirectDomains')}
</div>
<Input.TextArea
rows={2}
value={directDomains}
placeholder="domain:ir, domain:cn, example.local"
onChange={(e) => setDirectDomains(e.target.value)}
/>
</div>
<div>
<div style={{ fontWeight: 600, marginBottom: 4 }}>
{t('pages.settings.subHappProxyDomains')}
</div>
<Input.TextArea
rows={2}
value={proxyDomains}
placeholder="geosite:google, youtube.com"
onChange={(e) => setProxyDomains(e.target.value)}
/>
</div>
<div>
<div style={{ fontWeight: 600, marginBottom: 4 }}>
{t('pages.settings.subHappBlockDomains')}
</div>
<Input.TextArea
rows={2}
value={blockDomains}
placeholder="geosite:category-ads-all, analytics.google.com"
onChange={(e) => setBlockDomains(e.target.value)}
/>
</div>
<div>
<div style={{ fontWeight: 600, marginBottom: 4 }}>
{t('pages.settings.subHappDirectIPs')}
</div>
<Input.TextArea
rows={2}
value={directIPs}
placeholder="geoip:ir, 192.168.0.0/16, 10.0.0.0/8"
onChange={(e) => setDirectIPs(e.target.value)}
/>
</div>
<div>
<div style={{ fontWeight: 600, marginBottom: 4 }}>
{t('pages.settings.subHappProxyIPs')}
</div>
<Input.TextArea
rows={2}
value={proxyIPs}
placeholder="1.1.1.1/32, 8.8.8.8/32"
onChange={(e) => setProxyIPs(e.target.value)}
/>
</div>
<div>
<div style={{ fontWeight: 600, marginBottom: 4 }}>
{t('pages.settings.subHappBlockIPs')}
</div>
<Input.TextArea
rows={2}
value={blockIPs}
placeholder="geoip:phishing, 0.0.0.0/8"
onChange={(e) => setBlockIPs(e.target.value)}
/>
</div>
</Space>
</Modal>
</>
);
}
@@ -17,6 +17,7 @@ import { RemarkTemplateField } from '@/components/form';
import { useMediaQuery } from '@/hooks/useMediaQuery';
import { catTabLabel } from './catTabLabel';
import { sanitizePath, normalizePath } from './uriPath';
import HappSettingsContent from './HappSettingsContent';
interface SubscriptionGeneralTabProps {
allSetting: AllSetting;
@@ -344,40 +345,12 @@ export default function SubscriptionGeneralTab({
key: '5',
label: catTabLabel(<BranchesOutlined />, 'Happ', isMobile),
children: (
<>
<SettingListItem
paddings="small"
title={t('pages.settings.subEnableRouting')}
description={t('pages.settings.subEnableRoutingDesc')}
>
<Switch
checked={allSetting.subEnableRouting}
onChange={(v) => updateSetting({ subEnableRouting: v })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subRoutingRules')}
badge={remoteSourceBadge(allSetting.subRoutingRules)}
description={t('pages.settings.subRoutingRulesDesc')}
>
<Input.TextArea
value={allSetting.subRoutingRules}
placeholder="happ://routing/onadd/... or https://.../DEFAULT.DEEPLINK"
onChange={(e) => updateSetting({ subRoutingRules: e.target.value })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHideSettings')}
description={t('pages.settings.subHideSettingsDesc')}
>
<Switch
checked={allSetting.subHideSettings}
onChange={(v) => updateSetting({ subHideSettings: v })}
/>
</SettingListItem>
</>
<HappSettingsContent
allSetting={allSetting}
updateSetting={updateSetting}
isMobile={isMobile}
remoteSourceBadge={remoteSourceBadge}
/>
),
},
{
+118
View File
@@ -0,0 +1,118 @@
// Encode UTF-8 string to standard Base64 safe for browser environments.
export function toBase64Utf8(str: string): string {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (_, p1) =>
String.fromCharCode(Number(`0x${p1}`)),
),
);
}
// Splits multiline or comma-separated string into clean unique token arrays.
export function parseList(input: string): string[] {
return input
.split(/[\n,]+/)
.map((s) => s.trim())
.filter(Boolean);
}
// Build standard Happ routing deeplink or special state for curated presets.
export function buildHappPresetDeeplink(preset: string): string {
switch (preset) {
case 'off':
return 'happ://routing/off';
case 'iran-bypass':
return (
'happ://routing/onadd/' +
toBase64Utf8(
JSON.stringify({
rules: [
{
type: 'field',
outboundTag: 'direct',
domain: ['domain:ir', 'regexp:.*\\.ir$'],
ip: ['geoip:ir', 'geoip:private'],
},
{
type: 'field',
outboundTag: 'block',
domain: ['geosite:category-ads-all'],
},
{
type: 'field',
outboundTag: 'proxy',
network: 'tcp,udp',
},
],
}),
)
);
case 'china-direct':
return (
'happ://routing/onadd/' +
toBase64Utf8(
JSON.stringify({
rules: [
{
type: 'field',
outboundTag: 'direct',
domain: ['domain:cn', 'geosite:cn'],
ip: ['geoip:cn', 'geoip:private'],
},
{
type: 'field',
outboundTag: 'block',
domain: ['geosite:category-ads-all'],
},
{
type: 'field',
outboundTag: 'proxy',
network: 'tcp,udp',
},
],
}),
)
);
case 'adblock':
return (
'happ://routing/onadd/' +
toBase64Utf8(
JSON.stringify({
rules: [
{
type: 'field',
outboundTag: 'block',
domain: ['geosite:category-ads-all'],
},
{
type: 'field',
outboundTag: 'direct',
ip: ['geoip:private'],
},
{
type: 'field',
outboundTag: 'proxy',
network: 'tcp,udp',
},
],
}),
)
);
case 'global':
return (
'happ://routing/onadd/' +
toBase64Utf8(
JSON.stringify({
rules: [
{
type: 'field',
outboundTag: 'proxy',
network: 'tcp,udp',
},
],
}),
)
);
default:
return '';
}
}