Skip to content

Commit d26d679

Browse files
committed
feat(core-editor): add VLESS reverse proxy support with validation
1 parent b80937b commit d26d679

3 files changed

Lines changed: 90 additions & 0 deletions

File tree

dashboard/src/features/core-editor/components/xray/xray-outbounds-section.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,20 @@ function isJsonObject(value: unknown): value is JsonObject {
221221
return typeof value === 'object' && value !== null && !Array.isArray(value)
222222
}
223223

224+
function vlessReverseTagFromSettings(settings: Record<string, unknown>): string {
225+
const reverse = settings.reverse
226+
if (typeof reverse === 'string') return reverse.trim()
227+
if (!reverse || typeof reverse !== 'object' || Array.isArray(reverse)) return ''
228+
const tag = (reverse as Record<string, unknown>).tag
229+
return typeof tag === 'string' ? tag.trim() : ''
230+
}
231+
232+
function hasVlessReverseSettings(settings: Record<string, unknown>): boolean {
233+
const reverse = settings.reverse
234+
if (typeof reverse === 'string') return reverse.trim().length > 0
235+
return !!reverse && typeof reverse === 'object' && !Array.isArray(reverse)
236+
}
237+
224238
function uniqueOutboundTags(outbounds: Outbound[] | undefined): string[] {
225239
return [...new Set((outbounds ?? []).map(outbound => String(outbound.tag ?? '').trim()).filter(Boolean))]
226240
}
@@ -336,6 +350,19 @@ function collectOutboundRequiredIssues(ob: Outbound, t: (key: string, opts?: Rec
336350
const p = ob.protocol
337351
if (PROXY_ENDPOINT_PROTOCOLS.has(p)) {
338352
const s = flattenOutboundSettings(ob)
353+
354+
if (p === 'vless' && hasVlessReverseSettings(s)) {
355+
if (!vlessReverseTagFromSettings(s)) {
356+
issues.push({
357+
field: 'reverse',
358+
message: t('coreEditor.outbound.validation.reverseTagRequired', {
359+
defaultValue: 'Reverse outbound tag is required.',
360+
}),
361+
})
362+
}
363+
return issues
364+
}
365+
339366
const address = String(s.address ?? '').trim()
340367
const portRaw = s.port
341368
const portNum = typeof portRaw === 'number' ? portRaw : Number(portRaw)
@@ -1238,6 +1265,23 @@ function OutboundProxyEndpointSection({ ob, patchOutbound, t }: OutboundProxyEnd
12381265
if (!vlessVisionFlowIncompatibleWithStreamSecurity(streamSecForFlow, flowStr)) return
12391266
patchOutboundWithSettingsMerge(ob, patchOutbound, s => ({ ...s, flow: '' }))
12401267
}, [p, ob, flowStr, streamSecForFlow, patchOutbound])
1268+
1269+
if (p === 'vless' && hasVlessReverseSettings(flat)) {
1270+
const reverseTag = vlessReverseTagFromSettings(flat)
1271+
return (
1272+
<div className="rounded-md border p-4">
1273+
<p className="text-sm font-medium">{t('coreEditor.outbound.reverseSection', { defaultValue: 'Reverse proxy' })}</p>
1274+
<p className="mt-2 text-xs text-muted-foreground">
1275+
{t('coreEditor.outbound.reverseHint', {
1276+
defaultValue: reverseTag
1277+
? `Traffic routed to this outbound is forwarded through reverse tag "${reverseTag}".`
1278+
: 'Set a reverse tag below. VLESS reverse mode does not use address, port, UUID, or encryption fields.',
1279+
})}
1280+
</p>
1281+
</div>
1282+
)
1283+
}
1284+
12411285
const address = String(flat.address ?? '')
12421286
const portStr = flat.port != null && flat.port !== '' ? String(flat.port) : ''
12431287
const id = String(flat.id ?? '')

dashboard/src/features/core-editor/kit/outbound-editor-json.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,25 @@ function compactSettingsUser(user: Record<string, unknown>): Record<string, unkn
7272
return out
7373
}
7474

75+
function normalizeVlessReverseSetting(value: unknown): Record<string, unknown> | undefined {
76+
if (value === undefined || value === null) return undefined
77+
if (typeof value === 'string') {
78+
const trimmed = value.trim()
79+
if (!trimmed) return undefined
80+
try {
81+
const parsed = JSON.parse(trimmed) as unknown
82+
return normalizeVlessReverseSetting(parsed)
83+
} catch {
84+
return { tag: trimmed }
85+
}
86+
}
87+
if (typeof value !== 'object' || Array.isArray(value)) return undefined
88+
89+
const pruned = deepPruneEmptyJsonObjects(value) as unknown
90+
if (!pruned || typeof pruned !== 'object' || Array.isArray(pruned)) return undefined
91+
return Object.keys(pruned as Record<string, unknown>).length > 0 ? (pruned as Record<string, unknown>) : undefined
92+
}
93+
7594
function flattenVnextLike(
7695
settings: Record<string, unknown>,
7796
protocol: 'vless' | 'vmess',
@@ -233,8 +252,12 @@ export function outboundEditorBodyFromOutbound(ob: Outbound): Record<string, unk
233252
export function normalizeSettingsFromEditor(protocol: string, settings: Record<string, unknown>): Record<string, unknown> {
234253
if (protocol === 'vless') {
235254
const next = { ...settings }
255+
const reverse = normalizeVlessReverseSetting(next.reverse)
256+
if (reverse) return { reverse }
257+
236258
if (typeof next.flow === 'string' && next.flow === '') delete next.flow
237259
if (Array.isArray(next.vnext) && next.vnext.length === 0) delete next.vnext
260+
delete next.reverse
238261
for (const key of ['address', 'port', 'id', 'encryption', 'level', 'email'] as const) {
239262
if (next[key] === undefined || next[key] === null || next[key] === '') delete next[key]
240263
}

dashboard/src/features/core-editor/kit/xray-parity-value.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ function normParityFieldKey(field: XrayGeneratedFormField): string {
194194
.toLowerCase()
195195
}
196196

197+
function isVlessReverseField(field: XrayGeneratedFormField): boolean {
198+
return normParityFieldKey(field) === 'reverse' && field.type.replace(/^\*+/, '') === 'VLessReverseConfig'
199+
}
200+
197201
/** Keep only supported curve names; order is not preserved (caller may re-order). */
198202
export function filterTlsCurvePreferenceStrings(values: readonly string[]): string[] {
199203
const allowed = new Set<string>(TLS_CURVE_PREFERENCE_OPTIONS as readonly string[])
@@ -210,6 +214,11 @@ export function filterTlsCurvePreferenceStrings(values: readonly string[]): stri
210214
}
211215

212216
export function outboundSettingToString(value: unknown, field: XrayGeneratedFormField): string {
217+
if (isVlessReverseField(field) && value && typeof value === 'object' && !Array.isArray(value)) {
218+
const tag = (value as Record<string, unknown>).tag
219+
if (typeof tag === 'string') return tag
220+
}
221+
213222
const mode = inferParityFieldMode(field)
214223
if (value === undefined || value === null) return ''
215224
if (mode === 'json') {
@@ -226,6 +235,20 @@ export function outboundSettingToString(value: unknown, field: XrayGeneratedForm
226235
}
227236

228237
export function parseOutboundSettingValue(field: XrayGeneratedFormField, raw: string): unknown {
238+
if (isVlessReverseField(field)) {
239+
const t = raw.trim()
240+
if (!t) return undefined
241+
try {
242+
const parsed = JSON.parse(t) as unknown
243+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
244+
return deepPruneEmptyJsonObjects(parsed) as unknown
245+
}
246+
} catch {
247+
// Treat plain text as the reverse outbound tag.
248+
}
249+
return { tag: t }
250+
}
251+
229252
const mode = inferParityFieldMode(field)
230253
const t = raw.trim()
231254
if (mode === 'json') {

0 commit comments

Comments
 (0)