mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-08 14:36:13 +00:00
refactor(fallbacks): share template, tighter UX, cleaner JSON
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -145,6 +145,19 @@ class XrayCommonClass {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build a clean Xray fallback entry. Per docs, name/alpn/path empty = "any",
|
||||||
|
// and xver=0 means PROXY protocol off — omit them so the generated config
|
||||||
|
// stays minimal and readable. dest is required and always emitted.
|
||||||
|
static fallbackToJson(fb) {
|
||||||
|
const out = { dest: fb.dest };
|
||||||
|
if (fb.name) out.name = fb.name;
|
||||||
|
if (fb.alpn) out.alpn = fb.alpn;
|
||||||
|
if (fb.path) out.path = fb.path;
|
||||||
|
const xver = Number(fb.xver);
|
||||||
|
if (Number.isInteger(xver) && xver > 0) out.xver = xver;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
toString(format = true) {
|
toString(format = true) {
|
||||||
return format ? JSON.stringify(this.toJson(), null, 2) : JSON.stringify(this.toJson());
|
return format ? JSON.stringify(this.toJson(), null, 2) : JSON.stringify(this.toJson());
|
||||||
}
|
}
|
||||||
@@ -2733,31 +2746,13 @@ Inbound.VLESSSettings.Fallback = class extends XrayCommonClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toJson() {
|
toJson() {
|
||||||
let xver = this.xver;
|
return XrayCommonClass.fallbackToJson(this);
|
||||||
if (!Number.isInteger(xver)) {
|
|
||||||
xver = 0;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name: this.name,
|
|
||||||
alpn: this.alpn,
|
|
||||||
path: this.path,
|
|
||||||
dest: this.dest,
|
|
||||||
xver: xver,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json = []) {
|
static fromJson(json = []) {
|
||||||
const fallbacks = [];
|
return (json || []).map(f => new Inbound.VLESSSettings.Fallback(
|
||||||
for (let fallback of json) {
|
f.name, f.alpn, f.path, f.dest, f.xver,
|
||||||
fallbacks.push(new Inbound.VLESSSettings.Fallback(
|
));
|
||||||
fallback.name,
|
|
||||||
fallback.alpn,
|
|
||||||
fallback.path,
|
|
||||||
fallback.dest,
|
|
||||||
fallback.xver,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
return fallbacks;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2786,10 +2781,13 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toJson() {
|
toJson() {
|
||||||
return {
|
const json = {
|
||||||
clients: Inbound.TrojanSettings.toJsonArray(this.trojans),
|
clients: Inbound.TrojanSettings.toJsonArray(this.trojans),
|
||||||
fallbacks: Inbound.TrojanSettings.toJsonArray(this.fallbacks)
|
|
||||||
};
|
};
|
||||||
|
if (this.fallbacks && this.fallbacks.length > 0) {
|
||||||
|
json.fallbacks = Inbound.TrojanSettings.toJsonArray(this.fallbacks);
|
||||||
|
}
|
||||||
|
return json;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2828,31 +2826,13 @@ Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toJson() {
|
toJson() {
|
||||||
let xver = this.xver;
|
return XrayCommonClass.fallbackToJson(this);
|
||||||
if (!Number.isInteger(xver)) {
|
|
||||||
xver = 0;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name: this.name,
|
|
||||||
alpn: this.alpn,
|
|
||||||
path: this.path,
|
|
||||||
dest: this.dest,
|
|
||||||
xver: xver,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json = []) {
|
static fromJson(json = []) {
|
||||||
const fallbacks = [];
|
return (json || []).map(f => new Inbound.TrojanSettings.Fallback(
|
||||||
for (let fallback of json) {
|
f.name, f.alpn, f.path, f.dest, f.xver,
|
||||||
fallbacks.push(new Inbound.TrojanSettings.Fallback(
|
));
|
||||||
fallback.name,
|
|
||||||
fallback.alpn,
|
|
||||||
fallback.path,
|
|
||||||
fallback.dest,
|
|
||||||
fallback.xver,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
return fallbacks;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
85
web/html/form/fallbacks.html
Normal file
85
web/html/form/fallbacks.html
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
{{define "form/fallbacks"}}
|
||||||
|
<div :style="{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '8px', margin: '8px 0' }">
|
||||||
|
<span :style="{ fontWeight: 500 }">
|
||||||
|
<a-tooltip title="Route incoming TLS traffic to a backend when it doesn't match a valid VLESS/Trojan handshake. Match by SNI, ALPN, and HTTP path; the most precise rule wins. Fallbacks require TCP+TLS transport.">
|
||||||
|
Fallbacks ([[ inbound.settings.fallbacks.length ]])
|
||||||
|
<a-icon type="question-circle"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<span :style="{ flex: 1 }"></span>
|
||||||
|
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()">Add</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false"
|
||||||
|
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||||
|
<a-divider :style="{ margin: '0' }">
|
||||||
|
Fallback [[ index + 1 ]]
|
||||||
|
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
|
||||||
|
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer', marginLeft: '6px' }"></a-icon>
|
||||||
|
</a-divider>
|
||||||
|
|
||||||
|
<a-form-item>
|
||||||
|
<template slot="label">
|
||||||
|
<a-tooltip title="Match TLS SNI (server name). Leave empty to match any SNI.">
|
||||||
|
SNI <a-icon type="question-circle"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<a-input v-model.trim="fallback.name" placeholder="any (leave empty)"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item>
|
||||||
|
<template slot="label">
|
||||||
|
<a-tooltip title="Match TLS ALPN. 'any' = no ALPN constraint. Use h2/http/1.1 split when the inbound advertises both.">
|
||||||
|
ALPN <a-icon type="question-circle"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<a-select v-model="fallback.alpn" :style="{ width: '100%' }">
|
||||||
|
<a-select-option value="">any</a-select-option>
|
||||||
|
<a-select-option value="h2">h2</a-select-option>
|
||||||
|
<a-select-option value="http/1.1">http/1.1</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item
|
||||||
|
:validate-status="fallback.path && !fallback.path.startsWith('/') ? 'error' : ''"
|
||||||
|
:help="fallback.path && !fallback.path.startsWith('/') ? 'Path must start with /' : ''">
|
||||||
|
<template slot="label">
|
||||||
|
<a-tooltip title="Match the HTTP request path of the first packet. Must start with '/'. Leave empty to match any.">
|
||||||
|
Path <a-icon type="question-circle"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<a-input v-model.trim="fallback.path" placeholder="any (leave empty) or /ws"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item
|
||||||
|
:validate-status="!fallback.dest ? 'error' : ''"
|
||||||
|
:help="!fallback.dest ? 'Destination is required' : ''">
|
||||||
|
<template slot="label">
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>
|
||||||
|
Where matching traffic is forwarded. Accepts a port number (<code>80</code>),
|
||||||
|
an <code>addr:port</code> (<code>127.0.0.1:8080</code>), or a Unix socket path
|
||||||
|
(<code>/dev/shm/x.sock</code> or <code>@abstract</code>).
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
Destination <a-icon type="question-circle"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<a-input v-model.trim="fallback.dest" placeholder="80 | 127.0.0.1:8080 | /dev/shm/x.sock"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item>
|
||||||
|
<template slot="label">
|
||||||
|
<a-tooltip title="PROXY protocol version sent to the destination. Off (0) for plain TCP; v1/v2 to preserve client IP if the backend supports it.">
|
||||||
|
PROXY <a-icon type="question-circle"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<a-select v-model="fallback.xver" :style="{ width: '100%' }">
|
||||||
|
<a-select-option :value="0">Off</a-select-option>
|
||||||
|
<a-select-option :value="1">v1</a-select-option>
|
||||||
|
<a-select-option :value="2">v2</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
{{end}}
|
||||||
@@ -19,35 +19,7 @@
|
|||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
<template v-if=" inbound.isTcp">
|
<template v-if=" inbound.isTcp">
|
||||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
{{template "form/fallbacks" .}}
|
||||||
<a-form-item label="Fallbacks">
|
|
||||||
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
|
|
||||||
<!-- trojan fallbacks -->
|
|
||||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
|
|
||||||
:wrapper-col="{ md: {span:14} }">
|
|
||||||
<a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete"
|
|
||||||
@click="() => inbound.settings.delFallback(index)"
|
|
||||||
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
|
||||||
</a-divider>
|
|
||||||
<a-form-item label='SNI'>
|
|
||||||
<a-input v-model="fallback.name"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label='ALPN'>
|
|
||||||
<a-input v-model="fallback.alpn"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label='Path'>
|
|
||||||
<a-input v-model="fallback.path"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label='Dest'>
|
|
||||||
<a-input v-model="fallback.dest"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label='xVer'>
|
|
||||||
<a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
<a-divider style="margin:5px 0;"></a-divider>
|
<a-divider style="margin:5px 0;"></a-divider>
|
||||||
</template>
|
</template>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -42,35 +42,7 @@
|
|||||||
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="inbound.isTcp && (!inbound.settings.encryption || inbound.settings.encryption === 'none')">
|
<template v-if="inbound.isTcp && (!inbound.settings.encryption || inbound.settings.encryption === 'none')">
|
||||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
{{template "form/fallbacks" .}}
|
||||||
<a-form-item label="Fallbacks">
|
|
||||||
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
|
|
||||||
<!-- vless fallbacks -->
|
|
||||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
|
|
||||||
:wrapper-col="{ md: {span:14} }">
|
|
||||||
<a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete"
|
|
||||||
@click="() => inbound.settings.delFallback(index)"
|
|
||||||
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
|
||||||
</a-divider>
|
|
||||||
<a-form-item label='SNI'>
|
|
||||||
<a-input v-model="fallback.name"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label='ALPN'>
|
|
||||||
<a-input v-model="fallback.alpn"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label='Path'>
|
|
||||||
<a-input v-model="fallback.path"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label='Dest'>
|
|
||||||
<a-input v-model="fallback.dest"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label='xVer'>
|
|
||||||
<a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="inbound.canEnableVisionSeed()">
|
<template v-if="inbound.canEnableVisionSeed()">
|
||||||
|
|||||||
Reference in New Issue
Block a user