From ca4f32e3daf49b2f0af7ecd230911c82aff33585 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 10 Jun 2026 23:52:20 +0200 Subject: [PATCH] feat: replace panel proxy URL with outbound-based egress bridge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of requiring a manual SOCKS5/HTTP URL, the panel now lets the admin pick an Xray outbound from a dropdown (same UX as Geodata Auto-Update). At runtime, injectPanelEgress appends a loopback SOCKS inbound (tag: panel-egress) and prepends a routing rule so the panel's own HTTP traffic — version checks, Telegram, normal geo-file updates — is routed through the chosen outbound. Xray-native Geodata Auto-Update is unaffected (it uses its own geodata.outbound inside Xray). Blackhole outbounds are excluded from both picker dropdowns since routing any download through one just drops it. Translations updated for all 13 locales. --- frontend/public/openapi.json | 12 +- frontend/src/generated/examples.ts | 4 +- frontend/src/generated/schemas.ts | 12 +- frontend/src/generated/types.ts | 4 +- frontend/src/generated/zod.ts | 4 +- frontend/src/models/setting.ts | 2 +- frontend/src/pages/index/GeodataSection.tsx | 6 +- frontend/src/pages/settings/GeneralTab.tsx | 47 ++++++- frontend/src/schemas/setting.ts | 2 +- internal/web/controller/setting.go | 11 ++ internal/web/entity/entity.go | 2 +- ...nel_proxy_test.go => panel_egress_test.go} | 2 +- internal/web/service/setting.go | 52 ++++++-- internal/web/service/tgbot/tgbot.go | 11 +- internal/web/service/xray.go | 73 +++++++++++ .../web/service/xray_config_inject_test.go | 116 ++++++++++++++++++ internal/web/translation/ar-EG.json | 5 +- internal/web/translation/en-US.json | 5 +- internal/web/translation/es-ES.json | 5 +- internal/web/translation/fa-IR.json | 5 +- internal/web/translation/id-ID.json | 5 +- internal/web/translation/ja-JP.json | 5 +- internal/web/translation/pt-BR.json | 5 +- internal/web/translation/ru-RU.json | 5 +- internal/web/translation/tr-TR.json | 5 +- internal/web/translation/uk-UA.json | 5 +- internal/web/translation/vi-VN.json | 5 +- internal/web/translation/zh-CN.json | 5 +- internal/web/translation/zh-TW.json | 5 +- 29 files changed, 352 insertions(+), 73 deletions(-) rename internal/web/service/integration/{panel_proxy_test.go => panel_egress_test.go} (94%) diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index c33733fe1..8f014b715 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -120,8 +120,8 @@ "minimum": 0, "type": "integer" }, - "panelProxy": { - "description": "Proxy URL for the panel's own outbound requests (GitHub/Telegram)", + "panelOutbound": { + "description": "Xray outbound tag for the panel's own outbound HTTP (update checks/downloads, Telegram, geo updates, outbound-subscription fetches)", "type": "string" }, "remarkModel": { @@ -383,7 +383,7 @@ "ldapUserFilter", "ldapVlessField", "pageSize", - "panelProxy", + "panelOutbound", "remarkModel", "restartXrayOnClientDisable", "sessionMaxAge", @@ -554,8 +554,8 @@ "minimum": 0, "type": "integer" }, - "panelProxy": { - "description": "Proxy URL for the panel's own outbound requests (GitHub/Telegram)", + "panelOutbound": { + "description": "Xray outbound tag for the panel's own outbound HTTP (update checks/downloads, Telegram, geo updates, outbound-subscription fetches)", "type": "string" }, "remarkModel": { @@ -823,7 +823,7 @@ "ldapUserFilter", "ldapVlessField", "pageSize", - "panelProxy", + "panelOutbound", "remarkModel", "restartXrayOnClientDisable", "sessionMaxAge", diff --git a/frontend/src/generated/examples.ts b/frontend/src/generated/examples.ts index bb5c1b864..fc406257d 100644 --- a/frontend/src/generated/examples.ts +++ b/frontend/src/generated/examples.ts @@ -26,7 +26,7 @@ export const EXAMPLES: Record = { "ldapUserFilter": "", "ldapVlessField": "", "pageSize": 0, - "panelProxy": "", + "panelOutbound": "", "remarkModel": "", "restartXrayOnClientDisable": false, "sessionMaxAge": 1, @@ -115,7 +115,7 @@ export const EXAMPLES: Record = { "ldapUserFilter": "", "ldapVlessField": "", "pageSize": 0, - "panelProxy": "", + "panelOutbound": "", "remarkModel": "", "restartXrayOnClientDisable": false, "sessionMaxAge": 1, diff --git a/frontend/src/generated/schemas.ts b/frontend/src/generated/schemas.ts index 485f6afaf..7ff2a4aeb 100644 --- a/frontend/src/generated/schemas.ts +++ b/frontend/src/generated/schemas.ts @@ -94,8 +94,8 @@ export const SCHEMAS: Record = { "minimum": 0, "type": "integer" }, - "panelProxy": { - "description": "Proxy URL for the panel's own outbound requests (GitHub/Telegram)", + "panelOutbound": { + "description": "Xray outbound tag for the panel's own outbound HTTP (update checks/downloads, Telegram, geo updates, outbound-subscription fetches)", "type": "string" }, "remarkModel": { @@ -357,7 +357,7 @@ export const SCHEMAS: Record = { "ldapUserFilter", "ldapVlessField", "pageSize", - "panelProxy", + "panelOutbound", "remarkModel", "restartXrayOnClientDisable", "sessionMaxAge", @@ -528,8 +528,8 @@ export const SCHEMAS: Record = { "minimum": 0, "type": "integer" }, - "panelProxy": { - "description": "Proxy URL for the panel's own outbound requests (GitHub/Telegram)", + "panelOutbound": { + "description": "Xray outbound tag for the panel's own outbound HTTP (update checks/downloads, Telegram, geo updates, outbound-subscription fetches)", "type": "string" }, "remarkModel": { @@ -797,7 +797,7 @@ export const SCHEMAS: Record = { "ldapUserFilter", "ldapVlessField", "pageSize", - "panelProxy", + "panelOutbound", "remarkModel", "restartXrayOnClientDisable", "sessionMaxAge", diff --git a/frontend/src/generated/types.ts b/frontend/src/generated/types.ts index 842cf6cfd..e32ed7cf8 100644 --- a/frontend/src/generated/types.ts +++ b/frontend/src/generated/types.ts @@ -30,7 +30,7 @@ export interface AllSetting { ldapUserFilter: string; ldapVlessField: string; pageSize: number; - panelProxy: string; + panelOutbound: string; remarkModel: string; restartXrayOnClientDisable: boolean; sessionMaxAge: number; @@ -120,7 +120,7 @@ export interface AllSettingView { ldapUserFilter: string; ldapVlessField: string; pageSize: number; - panelProxy: string; + panelOutbound: string; remarkModel: string; restartXrayOnClientDisable: boolean; sessionMaxAge: number; diff --git a/frontend/src/generated/zod.ts b/frontend/src/generated/zod.ts index cf2eb0e4e..436d9f005 100644 --- a/frontend/src/generated/zod.ts +++ b/frontend/src/generated/zod.ts @@ -38,7 +38,7 @@ export const AllSettingSchema = z.object({ ldapUserFilter: z.string(), ldapVlessField: z.string(), pageSize: z.number().int().min(0).max(1000), - panelProxy: z.string(), + panelOutbound: z.string(), remarkModel: z.string(), restartXrayOnClientDisable: z.boolean(), sessionMaxAge: z.number().int().min(1).max(525600), @@ -129,7 +129,7 @@ export const AllSettingViewSchema = z.object({ ldapUserFilter: z.string(), ldapVlessField: z.string(), pageSize: z.number().int().min(0).max(1000), - panelProxy: z.string(), + panelOutbound: z.string(), remarkModel: z.string(), restartXrayOnClientDisable: z.boolean(), sessionMaxAge: z.number().int().min(1).max(525600), diff --git a/frontend/src/models/setting.ts b/frontend/src/models/setting.ts index f2d199070..33f5b93b5 100644 --- a/frontend/src/models/setting.ts +++ b/frontend/src/models/setting.ts @@ -9,7 +9,7 @@ export class AllSetting { webBasePath = '/'; sessionMaxAge = 360; trustedProxyCIDRs = '127.0.0.1/32,::1/128'; - panelProxy = ''; + panelOutbound = ''; pageSize = 25; expireDiff = 0; trafficDiff = 0; diff --git a/frontend/src/pages/index/GeodataSection.tsx b/frontend/src/pages/index/GeodataSection.tsx index 5facc1d8c..b41ae5970 100644 --- a/frontend/src/pages/index/GeodataSection.tsx +++ b/frontend/src/pages/index/GeodataSection.tsx @@ -65,10 +65,14 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti ); // Download outbound candidates: template outbounds + subscription outbounds. + // Skip blackhole outbounds — routing a download through one just drops it. const tags = new Set(); const outbounds = Array.isArray(template.outbounds) ? template.outbounds : []; for (const o of outbounds) { - const tag = o && typeof o === 'object' ? (o as Record).tag : undefined; + if (!o || typeof o !== 'object') continue; + const rec = o as Record; + if (rec.protocol === 'blackhole') continue; + const tag = rec.tag; if (typeof tag === 'string' && tag) tags.add(tag); } const subTags = Array.isArray(payload.subscriptionOutboundTags) diff --git a/frontend/src/pages/settings/GeneralTab.tsx b/frontend/src/pages/settings/GeneralTab.tsx index 16ca8ca13..bcf2150ec 100644 --- a/frontend/src/pages/settings/GeneralTab.tsx +++ b/frontend/src/pages/settings/GeneralTab.tsx @@ -43,6 +43,7 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp const [lang, setLang] = useState(() => LanguageManager.getLanguage()); const [inboundOptions, setInboundOptions] = useState<{ label: string; value: string }[]>([]); + const [outboundOptions, setOutboundOptions] = useState<{ label: string; value: string }[]>([]); useEffect(() => { let cancelled = false; @@ -65,6 +66,38 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp return () => { cancelled = true; }; }, []); + useEffect(() => { + let cancelled = false; + (async () => { + // Outbound tags for the panel egress picker: template outbounds plus + // subscription-derived outbounds, same candidate set as the geodata + // download picker. + const msg = await HttpUtil.post('/panel/api/xray/', undefined, { silent: true }) as ApiMsg; + if (cancelled || !msg?.success || typeof msg.obj !== 'string') return; + try { + const payload = JSON.parse(msg.obj) as Record; + const template = (payload.xraySetting || {}) as Record; + const tags = new Set(); + const outbounds = Array.isArray(template.outbounds) ? template.outbounds : []; + for (const o of outbounds) { + if (!o || typeof o !== 'object') continue; + const rec = o as Record; + if (rec.protocol === 'blackhole') continue; // dropping traffic is never a useful egress + const tag = rec.tag; + if (typeof tag === 'string' && tag) tags.add(tag); + } + const subTags = Array.isArray(payload.subscriptionOutboundTags) ? payload.subscriptionOutboundTags : []; + for (const tag of subTags) { + if (typeof tag === 'string' && tag) tags.add(tag); + } + setOutboundOptions([...tags].map((tag) => ({ label: tag, value: tag }))); + } catch { + setOutboundOptions([]); + } + })(); + return () => { cancelled = true; }; + }, []); + const ldapInboundTagList = useMemo(() => { const csv = allSetting.ldapInboundTags || ''; return csv.length ? csv.split(',').map((s) => s.trim()).filter(Boolean) : []; @@ -133,11 +166,15 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp /> - - updateSetting({ panelProxy: e.target.value })} + +