From 9f07951ba78e42d81bb64929f6c335fd41aa3262 Mon Sep 17 00:00:00 2001 From: Timur Chernykh <45117783+T3RR7@users.noreply.github.com> Date: Fri, 11 Sep 2026 16:32:04 +0300 Subject: [PATCH] feat(outbounds): support custom subscription user agents (#6398) Some subscription providers require a client-specific User-Agent before returning outbound links. Persist an optional value per subscription and use it for refreshes and previews while preserving the existing default for blank values. --- docs/public/openapi.json | 14 +++++++++ frontend/public/openapi.json | 14 +++++++++ frontend/src/pages/api-docs/endpoints.ts | 15 ++++++++++ .../src/pages/xray/outbounds/OutboundsTab.tsx | 22 +++++++++++++- internal/database/db.go | 11 +++++++ internal/database/model/model.go | 1 + internal/web/controller/xray_setting.go | 9 ++++-- internal/web/service/outbound_subscription.go | 14 +++++++-- .../web/service/outbound_subscription_test.go | 30 +++++++++++++++++-- internal/web/translation/ar-EG.json | 1 + internal/web/translation/en-US.json | 1 + internal/web/translation/es-ES.json | 1 + internal/web/translation/fa-IR.json | 1 + internal/web/translation/id-ID.json | 1 + internal/web/translation/ja-JP.json | 1 + internal/web/translation/pt-BR.json | 1 + internal/web/translation/ru-RU.json | 1 + internal/web/translation/tr-TR.json | 1 + internal/web/translation/uk-UA.json | 1 + internal/web/translation/vi-VN.json | 1 + internal/web/translation/zh-CN.json | 1 + internal/web/translation/zh-TW.json | 1 + 22 files changed, 134 insertions(+), 9 deletions(-) diff --git a/docs/public/openapi.json b/docs/public/openapi.json index ce1eabfce..d85a472f7 100644 --- a/docs/public/openapi.json +++ b/docs/public/openapi.json @@ -13569,6 +13569,11 @@ "type": "string", "description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub-\" prefix." }, + "userAgent": { + "type": "string", + "default": "3x-ui-outbound-sub/1.0", + "description": "Custom User-Agent sent when fetching this subscription. Defaults to \"3x-ui-outbound-sub/1.0\"." + }, "updateInterval": { "type": "integer", "default": 600, @@ -13662,6 +13667,11 @@ "type": "string", "description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub-\" prefix." }, + "userAgent": { + "type": "string", + "default": "3x-ui-outbound-sub/1.0", + "description": "Custom User-Agent sent when fetching this subscription. Defaults to \"3x-ui-outbound-sub/1.0\"." + }, "updateInterval": { "type": "integer", "default": 600, @@ -13914,6 +13924,10 @@ "type": "string", "description": "Subscription URL to preview (required)." }, + "userAgent": { + "type": "string", + "description": "Custom User-Agent sent while fetching the preview." + }, "allowPrivate": { "type": "boolean", "description": "Allow a private/internal/loopback URL. Default false." diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index ce1eabfce..d85a472f7 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -13569,6 +13569,11 @@ "type": "string", "description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub-\" prefix." }, + "userAgent": { + "type": "string", + "default": "3x-ui-outbound-sub/1.0", + "description": "Custom User-Agent sent when fetching this subscription. Defaults to \"3x-ui-outbound-sub/1.0\"." + }, "updateInterval": { "type": "integer", "default": 600, @@ -13662,6 +13667,11 @@ "type": "string", "description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub-\" prefix." }, + "userAgent": { + "type": "string", + "default": "3x-ui-outbound-sub/1.0", + "description": "Custom User-Agent sent when fetching this subscription. Defaults to \"3x-ui-outbound-sub/1.0\"." + }, "updateInterval": { "type": "integer", "default": 600, @@ -13914,6 +13924,10 @@ "type": "string", "description": "Subscription URL to preview (required)." }, + "userAgent": { + "type": "string", + "description": "Custom User-Agent sent while fetching the preview." + }, "allowPrivate": { "type": "boolean", "description": "Allow a private/internal/loopback URL. Default false." diff --git a/frontend/src/pages/api-docs/endpoints.ts b/frontend/src/pages/api-docs/endpoints.ts index 0bda0fe4a..44a5f51c9 100644 --- a/frontend/src/pages/api-docs/endpoints.ts +++ b/frontend/src/pages/api-docs/endpoints.ts @@ -90,6 +90,14 @@ const outboundSubscriptionBodyParams: EndpointParam[] = [ desc: 'Prefix for generated outbound tags. Defaults to the lowest free "sub-" prefix.', optional: true, }, + { + name: 'userAgent', + in: 'body (form)', + type: 'string', + desc: 'Custom User-Agent sent when fetching this subscription. Defaults to "3x-ui-outbound-sub/1.0".', + optional: true, + defaultValue: '3x-ui-outbound-sub/1.0', + }, { name: 'updateInterval', in: 'body (form)', @@ -2502,6 +2510,13 @@ export const sections: readonly Section[] = [ type: 'string', desc: 'Subscription URL to preview (required).', }, + { + name: 'userAgent', + in: 'body (form)', + type: 'string', + desc: 'Custom User-Agent sent while fetching the preview.', + optional: true, + }, { name: 'allowPrivate', in: 'body (form)', diff --git a/frontend/src/pages/xray/outbounds/OutboundsTab.tsx b/frontend/src/pages/xray/outbounds/OutboundsTab.tsx index fd4d2230e..fadadaa62 100644 --- a/frontend/src/pages/xray/outbounds/OutboundsTab.tsx +++ b/frontend/src/pages/xray/outbounds/OutboundsTab.tsx @@ -62,6 +62,8 @@ import { useOutboundColumns } from './useOutboundColumns'; import OutboundCardList from './OutboundCardList'; import SubscriptionOutbounds from './SubscriptionOutbounds'; +const defaultOutboundSubscriptionUserAgent = '3x-ui-outbound-sub/1.0'; + interface OutboundSub { id: number; remark?: string; @@ -69,6 +71,7 @@ interface OutboundSub { enabled?: boolean; allowPrivate?: boolean; allowInsecure?: boolean; + userAgent?: string; prepend?: boolean; priority?: number; tagPrefix?: string; @@ -136,6 +139,7 @@ export default function OutboundsTab({ remark: '', url: '', tagPrefix: '', + userAgent: '', updateInterval: 600, enabled: true, allowPrivate: false, @@ -334,6 +338,7 @@ export default function OutboundsTab({ remark?: string; url?: string; tagPrefix?: string; + userAgent?: string; updateInterval?: number; enabled?: boolean; allowPrivate?: boolean; @@ -344,6 +349,7 @@ export default function OutboundsTab({ remark: src.remark ?? '', url: src.url ?? '', tagPrefix: src.tagPrefix ?? '', + userAgent: src.userAgent ?? '', updateInterval: src.updateInterval ?? 600, enabled: src.enabled ?? true, allowPrivate: src.allowPrivate ?? false, @@ -356,6 +362,7 @@ export default function OutboundsTab({ remark: '', url: '', tagPrefix: '', + userAgent: '', updateInterval: 600, enabled: true, allowPrivate: false, @@ -370,6 +377,7 @@ export default function OutboundsTab({ remark: sub.remark ?? '', url: sub.url ?? '', tagPrefix: sub.tagPrefix ?? '', + userAgent: sub.userAgent ?? '', updateInterval: sub.updateInterval ?? 600, enabled: sub.enabled ?? true, allowPrivate: sub.allowPrivate ?? false, @@ -423,7 +431,12 @@ export default function OutboundsTab({ try { const r = await HttpUtil.post<{ tag?: string; protocol?: string }[]>( '/panel/api/xray/outbound-subs/parse', - { url: newSub.url, allowPrivate: newSub.allowPrivate }, + { + url: newSub.url, + userAgent: newSub.userAgent, + allowPrivate: newSub.allowPrivate, + allowInsecure: newSub.allowInsecure, + }, ); if (r?.success && Array.isArray(r.obj)) { setPreviewData(r.obj); @@ -721,6 +734,13 @@ export default function OutboundsTab({ placeholder={t('pages.xray.outboundSub.tagPrefixPlaceholder')} /> + + setNewSub({ ...newSub, userAgent: e.target.value })} + placeholder={defaultOutboundSubscriptionUserAgent} + /> +