Skip to content

Commit 5a65d40

Browse files
committed
fix(core-editor): make inbound port optional and improve VLESS encryption handling
1 parent 6c4155f commit 5a65d40

3 files changed

Lines changed: 67 additions & 24 deletions

File tree

dashboard/src/features/core-editor/kit/inbound-dialog-schema.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,10 @@ export function validateRealityServerNamesFormRaw(raw: unknown, t: TFunction): s
6565
}
6666

6767
/** Basics validated like host port/remark: required fields + numeric port range. Dynamic form keys pass through. */
68-
function allowsPortlessInboundListen(value: unknown): boolean {
69-
return typeof value === 'string' && value.trim().startsWith('@')
70-
}
71-
7268
export function createInboundDialogSchema(caps: Caps, t: TFunction) {
7369
const allowedProtocols = caps.protocolOrder.filter(p => caps.protocols[p])
7470
const protocolLabel = t('coreEditor.field.protocol', { defaultValue: 'Protocol' })
7571
const tagLabel = t('coreEditor.field.tag', { defaultValue: 'Tag' })
76-
const portLabel = t('coreEditor.field.port', { defaultValue: 'Port' })
7772

7873
const required = (fieldLabel: string) =>
7974
t('validation.required', { field: fieldLabel, defaultValue: `${fieldLabel} is required` })
@@ -95,18 +90,9 @@ export function createInboundDialogSchema(caps: Caps, t: TFunction) {
9590
})
9691
.passthrough()
9792
.superRefine((data, ctx) => {
98-
const p = typeof data.protocol === 'string' ? data.protocol.trim() : ''
99-
if (p === 'tun') return
10093
const rawPort = typeof data.port === 'string' ? data.port.trim() : ''
101-
if (rawPort.length === 0) {
102-
if (allowsPortlessInboundListen((data as Record<string, unknown>).listen)) return
103-
ctx.addIssue({
104-
code: z.ZodIssueCode.custom,
105-
message: required(portLabel),
106-
path: ['port'],
107-
})
108-
return
109-
}
94+
// Inbound port is optional; only validate format when a value is provided.
95+
if (rawPort.length === 0) return
11096
if (!isValidXrayPortList(rawPort)) {
11197
ctx.addIssue({
11298
code: z.ZodIssueCode.custom,

dashboard/src/features/core-editor/kit/xray-adapter.ts

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,49 @@ function isJsonValue(value: unknown): value is JsonValue {
2626
return Object.values(value).every(v => v === undefined || isJsonValue(v))
2727
}
2828

29+
function asRecord(value: unknown): Record<string, unknown> | null {
30+
return isRecord(value) ? value : null
31+
}
32+
33+
function inboundSettingString(rawInbound: unknown, key: string): string | undefined {
34+
const settings = asRecord(asRecord(rawInbound)?.settings)
35+
const value = settings?.[key]
36+
return typeof value === 'string' ? value : undefined
37+
}
38+
39+
function patchVlessInboundEncryptionFromRaw(profile: Profile, raw: unknown): Profile {
40+
if (!isRecord(raw)) return profile
41+
const rawInbounds = raw.inbounds
42+
if (!Array.isArray(rawInbounds)) return profile
43+
44+
const inbounds = profile.inbounds.map((inbound, index) => {
45+
if (inbound.protocol !== 'vless') return inbound
46+
const rawInbound = rawInbounds[index]
47+
const encryption = inboundSettingString(rawInbound, 'encryption')
48+
if (encryption === undefined) return inbound
49+
return { ...inbound, encryption } as typeof inbound
50+
})
51+
52+
return { ...profile, inbounds }
53+
}
54+
55+
function applyVlessInboundEncryptionToCompiledConfig(profile: Profile, config: Record<string, unknown>): Record<string, unknown> {
56+
if (!Array.isArray(config.inbounds)) return config
57+
const inbounds = config.inbounds.map((compiledInbound, index) => {
58+
if (!isRecord(compiledInbound) || compiledInbound.protocol !== 'vless') return compiledInbound
59+
const profileInbound = profile.inbounds?.[index]
60+
if (profileInbound?.protocol !== 'vless') return compiledInbound
61+
const encryption = typeof profileInbound.encryption === 'string' ? profileInbound.encryption : undefined
62+
if (encryption === undefined) return compiledInbound
63+
const normalizedEncryption = encryption.trim()
64+
if (normalizedEncryption === '' || normalizedEncryption === 'none') return compiledInbound
65+
const settings = isRecord(compiledInbound.settings) ? { ...compiledInbound.settings } : {}
66+
settings.encryption = encryption
67+
return { ...compiledInbound, settings }
68+
})
69+
return { ...config, inbounds }
70+
}
71+
2972
const UNMODELED_TOP_LEVEL_KEYS_TO_PRESERVE = [
3073
'policy',
3174
'api',
@@ -94,7 +137,7 @@ export type XrayPersistValidationResult =
94137
export function importRawToProfile(raw: unknown): { profile: Profile; issues: Issue[] } {
95138
const imported = importXrayConfig(raw)
96139
const profile = preserveUnmodeledTopLevelSections(
97-
sanitizeProfileInbounds(normalizeProfile(imported.profile)),
140+
patchVlessInboundEncryptionFromRaw(sanitizeProfileInbounds(normalizeProfile(imported.profile)), raw),
98141
raw,
99142
)
100143

@@ -104,7 +147,10 @@ export function importRawToProfile(raw: unknown): { profile: Profile; issues: Is
104147
export function profileToPersistedConfig(profile: Profile): Record<string, unknown> {
105148
const prepared = prepareProfileForKit(profile)
106149
const { config } = buildXrayConfig(prepared, { mode: 'permissive' })
107-
const result = applyInboundSockoptToCompiledConfig(prepared, config as Record<string, unknown>)
150+
const result = applyVlessInboundEncryptionToCompiledConfig(
151+
prepared,
152+
applyInboundSockoptToCompiledConfig(prepared, config as Record<string, unknown>),
153+
)
108154

109155
return result
110156
}
@@ -116,16 +162,19 @@ export function validateProfileForSave(profile: Profile) {
116162

117163
/**
118164
* Persist validation: strict-mode Xray compile blockers from xray-config-kit plus core-kit checks on permissive JSON
119-
* (inbound clients noise filtered out).
165+
* (inbound clients noise filtered out). Warnings and info-level issues do not block save.
120166
*/
121167
export function validateProfileForPersist(profile: Profile): XrayPersistValidationResult {
122168
const strictBlockers = getXrayStrictCompileBlockers(profile)
123169
const config = profileToPersistedConfig(profile)
124170
const r = validateCoreConfig('xray', config)
125171
const coreKitIssues = r.ok ? [] : filterCoreKitIssuesHidingInboundClients([...r.issues])
126172

127-
if (strictBlockers.length > 0 || coreKitIssues.length > 0) {
128-
return { ok: false, strictBlockers, coreKitIssues }
173+
const blockingStrict = strictBlockers.filter(i => i.severity !== 'warning' && i.severity !== 'info')
174+
const blockingCoreKit = coreKitIssues.filter(i => i.severity !== 'warning' && i.severity !== 'info')
175+
176+
if (blockingStrict.length > 0 || blockingCoreKit.length > 0) {
177+
return { ok: false, strictBlockers: blockingStrict, coreKitIssues: blockingCoreKit }
129178
}
130179
return { ok: true, config }
131180
}

dashboard/src/features/core-editor/routes/core-editor-page.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
88
import { CoreCommandMenu } from '@/features/core-editor/components/shared/core-command-menu'
99
import { CoreEditorLayout } from '@/features/core-editor/components/shell/core-editor-layout'
1010
import { CoreSectionTabsPlaceholder } from '@/features/core-editor/components/shell/core-section-sidebar'
11-
import { ValidationSummary, type ValidationListItem } from '@/features/core-editor/components/shared/validation-summary'
11+
import { filterValidationListBlockingErrors, ValidationSummary, type ValidationListItem } from '@/features/core-editor/components/shared/validation-summary'
1212
import type { SectionHeaderAddPulse } from '@/features/core-editor/hooks/use-section-header-add-pulse'
1313
import { useXrayPersistValidationItems } from '@/features/core-editor/hooks/use-xray-persist-validation-items'
1414
import { WireGuardCoreEditor } from '@/features/core-editor/components/wg/wireguard-core-editor'
@@ -284,7 +284,8 @@ export default function CoreEditorPage() {
284284
toast.error(t('coreEditor.nameRequired', { defaultValue: 'Name is required' }))
285285
return
286286
}
287-
if (preSaveIssues.length > 0) {
287+
const blockingPreSaveIssues = filterValidationListBlockingErrors(preSaveIssues)
288+
if (blockingPreSaveIssues.length > 0) {
288289
toast.error(t('coreEditor.fixValidation', { defaultValue: 'Fix validation errors before saving' }))
289290
return
290291
}
@@ -382,7 +383,14 @@ export default function CoreEditorPage() {
382383
}
383384
}
384385
} catch (e: unknown) {
385-
const msg = e instanceof Error ? e.message : String(e)
386+
const err = e as { data?: { detail?: unknown }; response?: { _data?: { detail?: unknown }; data?: { detail?: unknown } }; message?: string }
387+
const detail = err?.data?.detail ?? err?.response?._data?.detail ?? err?.response?.data?.detail
388+
const msg =
389+
typeof detail === 'string'
390+
? detail
391+
: detail
392+
? JSON.stringify(detail)
393+
: err?.message ?? String(e)
386394
toast.error(msg)
387395
} finally {
388396
setSaving(false)

0 commit comments

Comments
 (0)