feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links

A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.

Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.

Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.

Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.

Strings translated into all 12 non-English locales.
This commit is contained in:
MHSanaei 2026-05-19 01:11:09 +02:00
parent 1fe09e7764
commit 32966c1257
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
29 changed files with 958 additions and 133 deletions

24
.vscode/launch.json vendored
View file

@ -10,26 +10,12 @@
"program": "${workspaceFolder}",
"cwd": "${workspaceFolder}",
"env": {
"XUI_DEBUG": "true"
"XUI_DEBUG": "true",
"XUI_DB_FOLDER": "x-ui",
"XUI_LOG_FOLDER": "x-ui",
"XUI_BIN_FOLDER": "x-ui"
},
"console": "integratedTerminal"
},
{
"name": "Run 3x-ui (Debug, custom env)",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}",
"cwd": "${workspaceFolder}",
"env": {
// Set to true to serve assets/templates directly from disk for development
"XUI_DEBUG": "true",
// Uncomment to override DB folder location (by default uses working dir on Windows when debug)
// "XUI_DB_FOLDER": "${workspaceFolder}",
// Example: override log level (debug|info|notice|warn|error)
// "XUI_LOG_LEVEL": "debug"
},
"console": "integratedTerminal"
}
]
}
}

View file

@ -67,6 +67,7 @@ func initModels() error {
&model.ApiToken{},
&model.ClientRecord{},
&model.ClientInbound{},
&model.InboundFallback{},
}
for _, mdl := range models {
if err := db.AutoMigrate(mdl); err != nil {

View file

@ -35,6 +35,7 @@ func migrationModels() []any {
&model.InboundClientIps{},
&model.ClientRecord{},
&model.ClientInbound{},
&model.InboundFallback{},
}
}

View file

@ -66,6 +66,23 @@ type Inbound struct {
Tag string `json:"tag" form:"tag" gorm:"unique"`
Sniffing string `json:"sniffing" form:"sniffing"`
NodeID *int `json:"nodeId,omitempty" form:"nodeId" gorm:"index"`
// FallbackParent is populated by the API layer when this inbound is
// attached as a fallback child of a VLESS/Trojan TCP-TLS master.
// The frontend uses it to rewrite client-share links so they advertise
// the master's externally reachable endpoint instead of the child's
// loopback listen. Not persisted.
FallbackParent *FallbackParentInfo `json:"fallbackParent,omitempty" gorm:"-"`
}
// FallbackParentInfo carries everything the frontend needs to rewrite a
// child inbound's client link: where to connect (the master's address
// and port) and which path matched on the master's fallbacks array.
// The frontend already has the master inbound in its dbInbounds list,
// so we only ship identifiers + the match path here.
type FallbackParentInfo struct {
MasterId int `json:"masterId"`
Path string `json:"path,omitempty"`
}
// OutboundTraffics tracks traffic statistics for Xray outbound connections.
@ -362,6 +379,24 @@ type ClientInbound struct {
func (ClientInbound) TableName() string { return "client_inbounds" }
// InboundFallback is one routing rule on a master inbound's
// settings.fallbacks array. The master is always a VLESS or Trojan
// inbound on TCP transport with TLS or Reality. The child is any other
// inbound — its listen+port becomes the fallback dest, with optional
// SNI/ALPN/path match criteria pulled from the same row.
type InboundFallback struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
MasterId int `json:"masterId" gorm:"index;not null;column:master_id"`
ChildId int `json:"childId" gorm:"index;not null;column:child_id"`
Name string `json:"name"`
Alpn string `json:"alpn"`
Path string `json:"path"`
Xver int `json:"xver"`
SortOrder int `json:"sortOrder" gorm:"default:0;column:sort_order"`
}
func (InboundFallback) TableName() string { return "inbound_fallbacks" }
func (c *Client) ToRecord() *ClientRecord {
rec := &ClientRecord{
Email: c.Email,

View file

@ -40,6 +40,9 @@ export class DBInbound {
// Optional FK to web/runtime registered Node. null/undefined =
// local panel; otherwise the inbound lives on the named node.
this.nodeId = null;
// Populated by the API when this inbound is a fallback child of
// a VLESS/Trojan TCP-TLS master. Shape: { masterId, path }.
this.fallbackParent = null;
if (data == null) {
return;
}

View file

@ -150,6 +150,27 @@ export const sections = [
{ name: 'data', in: 'body (form)', type: 'string', desc: 'JSON-encoded inbound payload.' },
],
},
{
method: 'GET',
path: '/panel/api/inbounds/:id/fallbacks',
summary: 'List the fallback rules attached to a master VLESS/Trojan TCP-TLS inbound. Each rule links one child inbound (the dest) to optional SNI/ALPN/path/xver match criteria.',
params: [
{ name: 'id', in: 'path', type: 'number', desc: 'Master inbound ID.' },
],
response:
'{\n "success": true,\n "obj": [\n {\n "id": 1,\n "masterId": 10,\n "childId": 11,\n "name": "",\n "alpn": "",\n "path": "/vlws",\n "xver": 2,\n "sortOrder": 0\n }\n ]\n}',
},
{
method: 'POST',
path: '/panel/api/inbounds/:id/fallbacks',
summary: 'Replace the entire fallback list for a master inbound. Body is JSON. Triggers an Xray restart.',
params: [
{ name: 'id', in: 'path', type: 'number', desc: 'Master inbound ID.' },
{ name: 'fallbacks', in: 'body (json)', type: 'object[]', desc: 'Array of {childId, name, alpn, path, xver, sortOrder} entries.' },
],
body: '{\n "fallbacks": [\n { "childId": 11, "path": "/vlws", "xver": 2 },\n { "childId": 12, "alpn": "h2" }\n ]\n}',
response: '{\n "success": true,\n "msg": "Inbound updated"\n}',
},
],
},

View file

@ -3,7 +3,15 @@ import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import dayjs from 'dayjs';
import { message } from 'ant-design-vue';
import { SyncOutlined, PlusOutlined, MinusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import {
SyncOutlined,
PlusOutlined,
MinusOutlined,
DeleteOutlined,
CaretUpOutlined,
CaretDownOutlined,
SettingOutlined,
} from '@ant-design/icons-vue';
import {
HttpUtil,
@ -117,6 +125,193 @@ const isVlessLike = computed(() => {
return inbound.value.protocol === Protocols.VLESS;
});
const FALLBACK_ELIGIBLE_TRANSPORTS = new Set(['tcp', 'ws', 'grpc', 'httpupgrade', 'xhttp']);
const isFallbackHost = computed(() => {
const ib = inbound.value;
if (!ib) return false;
if (ib.protocol !== Protocols.VLESS && ib.protocol !== Protocols.TROJAN) return false;
if (ib.stream?.network !== 'tcp') return false;
const sec = ib.stream?.security;
return sec === 'tls' || sec === 'reality';
});
const fallbacks = ref([]);
let fallbackRowKey = 0;
const fallbackEditing = ref(new Set());
const fallbackChildOptions = computed(() => {
const list = props.dbInbounds || [];
const masterId = props.dbInbound?.id;
return list
.filter((ib) => ib.id !== masterId)
.map((ib) => ({
label: `${ib.remark || `#${ib.id}`} · ${ib.protocol}:${ib.port}`,
value: ib.id,
}));
});
function getChildStream(childDb) {
if (!childDb) return null;
try { return childDb.toInbound()?.stream || null; } catch (_e) { return null; }
}
function deriveFallbackDefaults(childDb) {
const out = { name: '', alpn: '', path: '', xver: 0 };
const stream = getChildStream(childDb);
if (!stream) return out;
switch (stream.network) {
case 'tcp': {
const tcp = stream.tcp;
if (tcp?.type === 'http') {
const p = tcp?.request?.path;
if (Array.isArray(p) && p.length) out.path = p[0];
}
if (tcp?.acceptProxyProtocol) out.xver = 2;
break;
}
case 'ws': {
out.path = stream.ws?.path || '';
if (stream.ws?.acceptProxyProtocol) out.xver = 2;
break;
}
case 'grpc': {
out.path = stream.grpc?.serviceName || '';
out.alpn = 'h2';
break;
}
case 'httpupgrade': {
out.path = stream.httpupgrade?.path || '';
if (stream.httpupgrade?.acceptProxyProtocol) out.xver = 2;
break;
}
case 'xhttp': {
out.path = stream.xhttp?.path || '';
break;
}
}
return out;
}
function addFallback(childId = null) {
const row = { rowKey: `fb-${++fallbackRowKey}`, childId: childId || null, name: '', alpn: '', path: '', xver: 0 };
if (childId) {
const child = (props.dbInbounds || []).find((ib) => ib.id === childId);
Object.assign(row, deriveFallbackDefaults(child));
}
fallbacks.value.push(row);
}
function removeFallback(idx) {
fallbacks.value.splice(idx, 1);
}
function moveFallback(idx, dir) {
const arr = fallbacks.value;
const j = idx + dir;
if (j < 0 || j >= arr.length) return;
const tmp = arr[idx];
arr[idx] = arr[j];
arr[j] = tmp;
}
function onFallbackChildPicked(record, childId) {
record.childId = childId;
const child = (props.dbInbounds || []).find((ib) => ib.id === childId);
const defaults = deriveFallbackDefaults(child);
record.name = defaults.name;
record.alpn = defaults.alpn;
record.path = defaults.path;
record.xver = defaults.xver;
}
function rederiveFallback(record) {
if (!record?.childId) return;
const child = (props.dbInbounds || []).find((ib) => ib.id === record.childId);
const defaults = deriveFallbackDefaults(child);
record.name = defaults.name;
record.alpn = defaults.alpn;
record.path = defaults.path;
record.xver = defaults.xver;
message.success(t('pages.inbounds.fallbacks.rederived') || 'Re-filled from child');
}
function quickAddAllFallbacks() {
const masterId = props.dbInbound?.id;
const list = props.dbInbounds || [];
const existing = new Set(fallbacks.value.map((r) => r.childId).filter(Boolean));
let added = 0;
for (const ib of list) {
if (ib.id === masterId) continue;
if (existing.has(ib.id)) continue;
const stream = getChildStream(ib);
if (!stream || !FALLBACK_ELIGIBLE_TRANSPORTS.has(stream.network)) continue;
addFallback(ib.id);
added += 1;
}
if (added > 0) {
message.success(t('pages.inbounds.fallbacks.quickAdded', { n: added }) || `Added ${added} fallback(s)`);
} else {
message.info(t('pages.inbounds.fallbacks.quickAddedNone') || 'No new eligible inbounds to add');
}
}
function isFallbackEditing(rowKey) { return fallbackEditing.value.has(rowKey); }
function toggleFallbackEdit(rowKey) {
const next = new Set(fallbackEditing.value);
if (next.has(rowKey)) next.delete(rowKey); else next.add(rowKey);
fallbackEditing.value = next;
}
function describeFallback(record) {
const parts = [];
if (record.name) parts.push(`SNI=${record.name}`);
if (record.alpn) parts.push(`ALPN=${record.alpn}`);
if (record.path) parts.push(`path=${record.path}`);
const condition = parts.length
? `${t('pages.inbounds.fallbacks.routesWhen') || 'Routes when'} ${parts.join(' · ')}`
: (t('pages.inbounds.fallbacks.defaultCatchAll') || 'Default — catches anything else');
const proxyTag = record.xver === 2 ? ' · PROXY v2' : record.xver === 1 ? ' · PROXY v1' : '';
return { condition, proxyTag };
}
async function loadFallbacks(masterId) {
fallbacks.value = [];
if (!masterId) return;
const msg = await HttpUtil.get(`/panel/api/inbounds/${masterId}/fallbacks`);
if (!msg?.success || !Array.isArray(msg.obj)) return;
fallbacks.value = msg.obj.map((r) => ({
rowKey: `fb-${++fallbackRowKey}`,
childId: r.childId,
name: r.name || '',
alpn: r.alpn || '',
path: r.path || '',
xver: r.xver || 0,
}));
}
async function saveFallbacks(masterId) {
if (!masterId) return true;
const payload = {
fallbacks: fallbacks.value
.filter((c) => c.childId)
.map((c, i) => ({
childId: c.childId,
name: c.name,
alpn: c.alpn,
path: c.path,
xver: Number(c.xver) || 0,
sortOrder: i,
})),
};
const msg = await HttpUtil.post(
`/panel/api/inbounds/${masterId}/fallbacks`,
payload,
{ headers: { 'Content-Type': 'application/json' } },
);
return !!msg?.success;
}
const canEnableStream = computed(() => inbound.value?.canEnableStream?.() === true);
const canEnableTls = computed(() => inbound.value?.canEnableTls?.() === true);
const canEnableReality = computed(() => inbound.value?.canEnableReality?.() === true);
@ -124,6 +319,7 @@ const canEnableReality = computed(() => inbound.value?.canEnableReality?.() ===
const hasProtocolTabContent = computed(() => {
if (!inbound.value) return false;
if (isVlessLike.value) return true;
if (isFallbackHost.value) return true;
switch (inbound.value.protocol) {
case Protocols.SHADOWSOCKS:
case Protocols.HTTP:
@ -184,12 +380,20 @@ function primeAdvancedJson() {
watch(() => props.open, (next) => {
if (!next) return;
fallbackEditing.value = new Set();
if (props.mode === 'edit' && props.dbInbound) {
loadFromDbInbound(props.dbInbound);
const proto = props.dbInbound.protocol;
if (proto === Protocols.VLESS || proto === Protocols.TROJAN) {
loadFallbacks(props.dbInbound.id);
} else {
fallbacks.value = [];
}
} else {
inbound.value = makeFreshInbound(Protocols.VLESS);
dbForm.value = freshDbForm();
primeAdvancedJson();
fallbacks.value = [];
}
activeTabKey.value = 'basic';
advancedSectionKey.value = 'all';
@ -655,6 +859,12 @@ async function submit() {
: '/panel/api/inbounds/add';
const msg = await HttpUtil.post(url, payload);
if (msg?.success) {
if (isFallbackHost.value) {
const masterId = props.mode === 'edit'
? props.dbInbound.id
: (msg.obj?.id || msg.obj?.Id);
if (masterId) await saveFallbacks(masterId);
}
emit('saved');
close();
}
@ -769,6 +979,81 @@ watch(() => inbound.value?.protocol, () => stampAdvancedTextFor('stream'));
</a-form-item>
</a-form>
<a-card v-if="isFallbackHost" size="small" class="mt-12"
:title="t('pages.inbounds.fallbacks.title') || 'Fallbacks'">
<a-typography-paragraph type="secondary" style="margin-bottom: 12px">
{{ t('pages.inbounds.fallbacks.help') || 'When a connection on this inbound does not match any client, route it to another inbound. Pick a child below and the routing fields (SNI / ALPN / path / xver) auto-fill from its transport — most setups need no further tweaking. Each child should listen on 127.0.0.1 with security=none.' }}
</a-typography-paragraph>
<template v-if="fallbacks.length === 0">
<a-empty :description="t('pages.inbounds.fallbacks.empty') || 'No fallbacks yet'" :image-style="{ height: '40px' }" style="margin: 8px 0 12px" />
</template>
<div v-for="(record, index) in fallbacks" :key="record.rowKey"
style="border: 1px solid var(--app-border-tertiary); border-radius: 6px; padding: 10px 12px; margin-bottom: 8px">
<a-row :gutter="8" align="middle" :wrap="false">
<a-col flex="none">
<a-space direction="vertical" :size="2">
<a-button size="small" type="text" :disabled="index === 0" @click="moveFallback(index, -1)">
<CaretUpOutlined />
</a-button>
<a-button size="small" type="text" :disabled="index === fallbacks.length - 1" @click="moveFallback(index, 1)">
<CaretDownOutlined />
</a-button>
</a-space>
</a-col>
<a-col flex="auto">
<a-select :value="record.childId" :options="fallbackChildOptions" :show-search="true"
:placeholder="t('pages.inbounds.fallbacks.pickInbound') || 'Pick an inbound'"
:filter-option="(input, option) => (option.label || '').toLowerCase().includes(input.toLowerCase())"
style="width: 100%" @change="(v) => onFallbackChildPicked(record, v)" />
<a-typography-text type="secondary" style="font-size: 12px; display: block; margin-top: 4px">
{{ describeFallback(record).condition }}{{ describeFallback(record).proxyTag }}
</a-typography-text>
</a-col>
<a-col flex="none">
<a-space :size="4">
<a-tooltip :title="t('pages.inbounds.fallbacks.rederive') || 'Re-fill from child'">
<a-button size="small" type="text" :disabled="!record.childId" @click="rederiveFallback(record)">
<SyncOutlined />
</a-button>
</a-tooltip>
<a-tooltip :title="isFallbackEditing(record.rowKey)
? (t('pages.inbounds.fallbacks.hideAdvanced') || 'Hide advanced')
: (t('pages.inbounds.fallbacks.editAdvanced') || 'Edit routing fields')">
<a-button size="small" type="text" @click="toggleFallbackEdit(record.rowKey)">
<SettingOutlined />
</a-button>
</a-tooltip>
<a-button size="small" type="text" danger @click="removeFallback(index)">
<DeleteOutlined />
</a-button>
</a-space>
</a-col>
</a-row>
<a-row v-if="isFallbackEditing(record.rowKey)" :gutter="8" style="margin-top: 8px">
<a-col :span="8">
<a-input v-model:value="record.name" addon-before="SNI" :placeholder="t('pages.inbounds.fallbacks.matchAny') || 'any'" />
</a-col>
<a-col :span="5">
<a-input v-model:value="record.alpn" addon-before="ALPN" :placeholder="t('pages.inbounds.fallbacks.matchAny') || 'any'" />
</a-col>
<a-col :span="7">
<a-input v-model:value="record.path" addon-before="Path" placeholder="/" />
</a-col>
<a-col :span="4">
<a-input-number v-model:value="record.xver" addon-before="xver" :min="0" :max="2" style="width: 100%" />
</a-col>
</a-row>
</div>
<a-space :size="8" style="margin-top: 4px" wrap>
<a-button size="small" @click="addFallback()">
<PlusOutlined /> {{ t('pages.inbounds.fallbacks.add') || 'Add fallback' }}
</a-button>
<a-button size="small" type="primary" ghost @click="quickAddAllFallbacks">
{{ t('pages.inbounds.fallbacks.quickAddAll') || 'Quick add all eligible' }}
</a-button>
</a-space>
</a-card>
<!-- Shadowsocks shared fields (method/network/ivCheck) -->
<a-form v-if="protocol === Protocols.SHADOWSOCKS" :colon="false" :label-col="{ sm: { span: 8 } }"
:wrapper-col="{ sm: { span: 14 } }" class="mt-12">

View file

@ -178,7 +178,8 @@ function exportInboundSubs(dbInbound) {
function exportAllLinks() {
const out = [];
for (const ib of dbInbounds.value) {
out.push(ib.genInboundLinks(remarkModel.value, hostOverrideFor(ib)));
const projected = checkFallback(ib);
out.push(projected.genInboundLinks(remarkModel.value, hostOverrideFor(ib)));
}
openText({
title: 'Export all inbound links',
@ -227,8 +228,18 @@ function importInbound() {
// the root inbound that owns the listen address so QRs/links carry
// the externally-reachable host:port and the right TLS state.
function checkFallback(dbInbound) {
// We don't keep parsed Inbounds in state right now (the page works
// off DBInbounds); compute on the fly.
// Path 1: panel-tracked fallback relationship (inbound_fallbacks row).
// The backend annotates each child inbound with fallbackParent so the
// child's client-share link advertises the master's reachable endpoint
// and inherits its TLS / Reality state.
const parent = dbInbound.fallbackParent;
if (parent?.masterId) {
const master = dbInbounds.value.find((ib) => ib.id === parent.masterId);
if (master) return projectChildThroughMaster(dbInbound, master);
}
// Path 2: legacy unix-socket convention (`@vless-ws` etc.) walk the
// VLESS/Trojan TCP inbounds and look for one whose settings.fallbacks
// references this child's listen address.
if (!dbInbound.listen?.startsWith?.('@')) return dbInbound;
for (const candidate of dbInbounds.value) {
if (candidate.id === dbInbound.id) continue;
@ -237,23 +248,30 @@ function checkFallback(dbInbound) {
if (!['trojan', 'vless'].includes(parsed.protocol)) continue;
const fallbacks = parsed.settings.fallbacks || [];
if (!fallbacks.find((f) => f.dest === dbInbound.listen)) continue;
// Build a one-off DBInbound copy with the parent's listen/port +
// copied stream so the link gen sees the public endpoint.
const projected = JSON.parse(JSON.stringify(dbInbound));
projected.listen = candidate.listen;
projected.port = candidate.port;
const inheritedStream = parsed.stream;
const ownInbound = dbInbound.toInbound();
ownInbound.stream.security = inheritedStream.security;
ownInbound.stream.tls = inheritedStream.tls;
ownInbound.stream.externalProxy = inheritedStream.externalProxy;
projected.streamSettings = ownInbound.stream.toString();
// Re-wrap so callers get the same DBInbound shape they had.
return new dbInbound.constructor(projected);
return projectChildThroughMaster(dbInbound, candidate);
}
return dbInbound;
}
// projectChildThroughMaster returns a one-off DBInbound copy whose
// listen/port + TLS/Reality state come from the master, while the
// protocol/transport/clients stay the child's. This is what makes a
// `vless://uuid@server:443?type=ws&path=/vlws&security=tls` link work
// for a child VLESS-WS bound to 127.0.0.1.
function projectChildThroughMaster(child, master) {
const projected = JSON.parse(JSON.stringify(child));
projected.listen = master.listen;
projected.port = master.port;
const masterStream = master.toInbound().stream;
const childInbound = child.toInbound();
childInbound.stream.security = masterStream.security;
childInbound.stream.tls = masterStream.tls;
childInbound.stream.reality = masterStream.reality;
childInbound.stream.externalProxy = masterStream.externalProxy;
projected.streamSettings = childInbound.stream.toString();
return new child.constructor(projected);
}
function findClientIndex(dbInbound, client) {
if (!client) return 0;
const inbound = dbInbound.toInbound();

View file

@ -41,6 +41,7 @@ func (p *LinkProvider) SubLinksForSubId(host, subId string) ([]string, error) {
func (p *LinkProvider) LinksForClient(host string, inbound *model.Inbound, email string) []string {
svc := p.build(host)
svc.projectThroughFallbackMaster(inbound)
return splitLinkLines(svc.GetLink(inbound, email))
}

View file

@ -50,14 +50,7 @@ func (s *SubClashService) GetClash(subId string, host string) (string, string, e
if clients == nil {
continue
}
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
listen, port, streamSettings, err := s.SubService.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
if err == nil {
inbound.Listen = listen
inbound.Port = port
inbound.StreamSettings = streamSettings
}
}
s.SubService.projectThroughFallbackMaster(inbound)
for _, client := range clients {
if client.SubID == subId {
_, clientTraffics = s.SubService.appendUniqueTraffic(seenEmails, clientTraffics, inbound.ClientStats, client.Email)

View file

@ -110,14 +110,7 @@ func (s *SubJsonService) GetJson(subId string, host string) (string, string, err
if clients == nil {
continue
}
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
listen, port, streamSettings, err := s.SubService.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
if err == nil {
inbound.Listen = listen
inbound.Port = port
inbound.StreamSettings = streamSettings
}
}
s.SubService.projectThroughFallbackMaster(inbound)
for _, client := range clients {
if client.SubID == subId {

View file

@ -92,14 +92,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C
if clients == nil {
continue
}
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
if err == nil {
inbound.Listen = listen
inbound.Port = port
inbound.StreamSettings = streamSettings
}
}
s.projectThroughFallbackMaster(inbound)
for _, client := range clients {
if client.SubID == subId {
if client.Enable {
@ -192,16 +185,89 @@ func (s *SubService) getFallbackMaster(dest string, streamSettings string) (stri
return "", 0, "", err
}
var stream map[string]any
json.Unmarshal([]byte(streamSettings), &stream)
var masterStream map[string]any
json.Unmarshal([]byte(inbound.StreamSettings), &masterStream)
stream["security"] = masterStream["security"]
stream["tlsSettings"] = masterStream["tlsSettings"]
stream["externalProxy"] = masterStream["externalProxy"]
modifiedStream, _ := json.MarshalIndent(stream, "", " ")
return inbound.Listen, inbound.Port, mergeStreamFromMaster(streamSettings, inbound.StreamSettings), nil
}
return inbound.Listen, inbound.Port, string(modifiedStream), nil
// projectThroughFallbackMaster mutates the inbound in place so its
// Listen/Port/StreamSettings reflect the externally reachable master
// when applicable. Covers both fallback mechanisms:
// - panel-tracked: an inbound_fallbacks row where child_id = inbound.Id
// - legacy unix-socket: inbound.Listen begins with "@" and some VLESS/
// Trojan inbound's settings.fallbacks references that listen address
//
// Returns true when a projection happened; sub services call this before
// generating links so a child VLESS-WS bound to 127.0.0.1 emits the
// master's :443 + TLS state instead of its own loopback endpoint.
func (s *SubService) projectThroughFallbackMaster(inbound *model.Inbound) bool {
if inbound == nil {
return false
}
db := database.GetDB()
var master *model.Inbound
var rule model.InboundFallback
if err := db.Where("child_id = ?", inbound.Id).
Order("sort_order ASC, id ASC").
First(&rule).Error; err == nil {
var m model.Inbound
if err := db.Where("id = ?", rule.MasterId).First(&m).Error; err == nil {
master = &m
}
}
if master == nil && len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
var m model.Inbound
if err := db.Model(model.Inbound{}).
Where("JSON_TYPE(settings, '$.fallbacks') = 'array'").
Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", inbound.Listen).
First(&m).Error; err == nil {
master = &m
}
}
if master == nil {
return false
}
inbound.StreamSettings = mergeStreamFromMaster(inbound.StreamSettings, master.StreamSettings)
inbound.Listen = master.Listen
inbound.Port = master.Port
return true
}
// mergeStreamFromMaster copies the master's security + tlsSettings +
// realitySettings + externalProxy onto the child's stream so the child's
// link advertises the master's TLS / Reality state. Transport (network
// + ws/grpc/etc. settings) stays the child's.
func mergeStreamFromMaster(childStream, masterStream string) string {
var stream map[string]any
json.Unmarshal([]byte(childStream), &stream)
if stream == nil {
stream = map[string]any{}
}
var mst map[string]any
json.Unmarshal([]byte(masterStream), &mst)
if mst == nil {
return childStream
}
stream["security"] = mst["security"]
if v, ok := mst["tlsSettings"]; ok {
stream["tlsSettings"] = v
} else {
delete(stream, "tlsSettings")
}
if v, ok := mst["realitySettings"]; ok {
stream["realitySettings"] = v
} else {
delete(stream, "realitySettings")
}
if v, ok := mst["externalProxy"]; ok {
stream["externalProxy"] = v
}
out, err := json.MarshalIndent(stream, "", " ")
if err != nil {
return childStream
}
return string(out)
}
// GetLink dispatches to the protocol-specific generator for one (inbound, client)

View file

@ -17,8 +17,9 @@ import (
// InboundController handles HTTP requests related to Xray inbounds management.
type InboundController struct {
inboundService service.InboundService
xrayService service.XrayService
inboundService service.InboundService
xrayService service.XrayService
fallbackService service.FallbackService
}
// NewInboundController creates a new InboundController and sets up its routes.
@ -62,6 +63,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.GET("/list", a.getInbounds)
g.GET("/options", a.getInboundOptions)
g.GET("/get/:id", a.getInbound)
g.GET("/:id/fallbacks", a.getFallbacks)
g.POST("/add", a.addInbound)
g.POST("/del/:id", a.delInbound)
@ -70,6 +72,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/:id/resetTraffic", a.resetInboundTraffic)
g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/import", a.importInbound)
g.POST("/:id/fallbacks", a.setFallbacks)
}
// getInbounds retrieves the list of inbounds for the logged-in user.
@ -330,3 +333,42 @@ func resolveHost(c *gin.Context) string {
return c.Request.Host
}
// getFallbacks returns the fallback rules attached to the master inbound.
func (a *InboundController) getFallbacks(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18nWeb(c, "get"), err)
return
}
rows, err := a.fallbackService.GetByMaster(id)
if err != nil {
jsonMsg(c, I18nWeb(c, "get"), err)
return
}
jsonObj(c, rows, nil)
}
// setFallbacks atomically replaces the master inbound's fallback list
// and triggers an Xray restart so the new settings.fallbacks take effect.
func (a *InboundController) setFallbacks(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
type body struct {
Fallbacks []service.FallbackInput `json:"fallbacks"`
}
var b body
if err := c.ShouldBindJSON(&b); err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
if err := a.fallbackService.SetByMaster(id, b.Fallbacks); err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
a.xrayService.SetToNeedRestart()
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), nil)
}

147
web/service/fallback.go Normal file
View file

@ -0,0 +1,147 @@
package service
import (
"fmt"
"strings"
"github.com/mhsanaei/3x-ui/v3/database"
"github.com/mhsanaei/3x-ui/v3/database/model"
"gorm.io/gorm"
)
type FallbackService struct{}
// FallbackInput is the payload shape POSTed by the inbound form.
type FallbackInput struct {
ChildId int `json:"childId"`
Name string `json:"name"`
Alpn string `json:"alpn"`
Path string `json:"path"`
Xver int `json:"xver"`
SortOrder int `json:"sortOrder"`
}
// GetByMaster returns every fallback rule attached to the master inbound.
func (s *FallbackService) GetByMaster(masterId int) ([]model.InboundFallback, error) {
var rows []model.InboundFallback
err := database.GetDB().
Where("master_id = ?", masterId).
Order("sort_order ASC, id ASC").
Find(&rows).Error
if err != nil {
return nil, err
}
return rows, nil
}
// GetParentForChild finds the first fallback rule that points at childId.
// Used by client-link generation: when a child inbound is attached as a
// fallback, its client links should advertise the master's address+port
// and TLS instead of the child's loopback listen.
func (s *FallbackService) GetParentForChild(childId int) (*model.InboundFallback, error) {
var row model.InboundFallback
err := database.GetDB().
Where("child_id = ?", childId).
Order("sort_order ASC, id ASC").
First(&row).Error
if err == gorm.ErrRecordNotFound {
return nil, nil
}
if err != nil {
return nil, err
}
return &row, nil
}
// SetByMaster replaces the master's entire fallback list atomically.
func (s *FallbackService) SetByMaster(masterId int, items []FallbackInput) error {
db := database.GetDB()
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("master_id = ?", masterId).Delete(&model.InboundFallback{}).Error; err != nil {
return err
}
for i, c := range items {
if c.ChildId <= 0 || c.ChildId == masterId {
continue
}
row := model.InboundFallback{
MasterId: masterId,
ChildId: c.ChildId,
Name: c.Name,
Alpn: c.Alpn,
Path: c.Path,
Xver: c.Xver,
SortOrder: c.SortOrder,
}
if row.SortOrder == 0 {
row.SortOrder = i
}
if err := tx.Create(&row).Error; err != nil {
return err
}
}
return nil
})
}
// BuildFallbacksJSON resolves the master's fallback rows into Xray's
// expected settings.fallbacks shape, looking up each child's listen+port
// to fill the dest field. Returns nil when the master has no rules.
func (s *FallbackService) BuildFallbacksJSON(tx *gorm.DB, masterId int) ([]map[string]any, error) {
if tx == nil {
tx = database.GetDB()
}
var rows []model.InboundFallback
err := tx.Where("master_id = ?", masterId).
Order("sort_order ASC, id ASC").
Find(&rows).Error
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, nil
}
childIds := make([]int, 0, len(rows))
for i := range rows {
childIds = append(childIds, rows[i].ChildId)
}
var children []model.Inbound
if err := tx.Where("id IN ?", childIds).Find(&children).Error; err != nil {
return nil, err
}
byId := make(map[int]*model.Inbound, len(children))
for i := range children {
byId[children[i].Id] = &children[i]
}
out := make([]map[string]any, 0, len(rows))
for _, r := range rows {
child, ok := byId[r.ChildId]
if !ok {
continue
}
listen := strings.TrimSpace(child.Listen)
if listen == "" || listen == "0.0.0.0" || listen == "::" || listen == "::0" {
listen = "127.0.0.1"
}
entry := map[string]any{
"dest": fmt.Sprintf("%s:%d", listen, child.Port),
}
if r.Name != "" {
entry["name"] = r.Name
}
if r.Alpn != "" {
entry["alpn"] = r.Alpn
}
if r.Path != "" {
entry["path"] = r.Path
}
if r.Xver > 0 {
entry["xver"] = r.Xver
}
out = append(out, entry)
}
return out, nil
}

View file

@ -24,8 +24,9 @@ import (
)
type InboundService struct {
xrayApi xray.XrayAPI
clientService ClientService
xrayApi xray.XrayAPI
clientService ClientService
fallbackService FallbackService
}
func (s *InboundService) runtimeFor(ib *model.Inbound) (runtime.Runtime, error) {
@ -130,9 +131,44 @@ func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
return nil, err
}
s.enrichClientStats(db, inbounds)
s.annotateFallbackParents(db, inbounds)
return inbounds, nil
}
// annotateFallbackParents fills FallbackParent on each inbound that is
// the child side of a fallback rule. One DB round-trip serves the full
// list — the frontend needs this to rewrite the child's client-share
// link so it points at the master's reachable endpoint.
func (s *InboundService) annotateFallbackParents(db *gorm.DB, inbounds []*model.Inbound) {
if len(inbounds) == 0 {
return
}
childIds := make([]int, 0, len(inbounds))
for _, ib := range inbounds {
childIds = append(childIds, ib.Id)
}
var rows []model.InboundFallback
if err := db.Where("child_id IN ?", childIds).
Order("sort_order ASC, id ASC").
Find(&rows).Error; err != nil {
return
}
first := make(map[int]model.InboundFallback, len(rows))
for _, r := range rows {
if _, ok := first[r.ChildId]; !ok {
first[r.ChildId] = r
}
}
for _, ib := range inbounds {
if r, ok := first[ib.Id]; ok {
ib.FallbackParent = &model.FallbackParentInfo{
MasterId: r.MasterId,
Path: r.Path,
}
}
}
}
// InboundOption is the lightweight projection of an inbound used by client UI
// pickers — only the fields needed to render labels, filter by protocol, and
// decide whether the XTLS Vision flow selector should appear. Keeping this
@ -202,6 +238,39 @@ func inboundCanEnableTlsFlow(protocol, streamSettings string) bool {
return stream.Security == "tls" || stream.Security == "reality"
}
// inboundCanHostFallbacks gates the settings.fallbacks injection.
// Xray only honors fallbacks on VLESS and Trojan inbounds carried over
// TCP transport with TLS or Reality security.
func inboundCanHostFallbacks(ib *model.Inbound) bool {
if ib == nil {
return false
}
if ib.Protocol != model.VLESS && ib.Protocol != model.Trojan {
return false
}
return inboundCanEnableTlsFlow(string(ib.Protocol), ib.StreamSettings) ||
(ib.Protocol == model.Trojan && trojanStreamSupportsFallbacks(ib.StreamSettings))
}
// trojanStreamSupportsFallbacks mirrors the Trojan side of the same gate
// (Trojan reuses XTLS-Vision capable streams: tcp + tls or reality).
func trojanStreamSupportsFallbacks(streamSettings string) bool {
if streamSettings == "" {
return false
}
var stream struct {
Network string `json:"network"`
Security string `json:"security"`
}
if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil {
return false
}
if stream.Network != "tcp" {
return false
}
return stream.Security == "tls" || stream.Security == "reality"
}
// GetAllInbounds retrieves all inbounds with client stats.
func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
db := database.GetDB()

View file

@ -193,6 +193,21 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
settings["clients"] = finalClients
}
if inboundCanHostFallbacks(inbound) {
fallbacks, fbErr := s.inboundService.fallbackService.BuildFallbacksJSON(nil, inbound.Id)
if fbErr != nil {
return nil, fbErr
}
if len(fallbacks) > 0 {
generic := make([]any, 0, len(fallbacks))
for _, f := range fallbacks {
generic = append(generic, f)
}
settings["fallbacks"] = generic
mutated = true
}
}
if mutated {
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {

View file

@ -250,11 +250,22 @@
"node": "نود",
"deployTo": "نشر على",
"localPanel": "بانل محلي",
"portFallback": {
"title": "العناصر الاحتياطية",
"help": "حدد الاتصالات الواردة التي يجب أن تستقبل حركة المرور التي لا تتطابق مع اتصال VLESS-TLS الوارد. يجب أن يستمع كل عنصر فرعي على 127.0.0.1 لاستقبال الاتصالات المُحوَّلة.",
"child": "الاتصال الوارد",
"path": "المسار"
"fallbacks": {
"title": "الـ Fallbacks",
"help": "عند وصول اتصال إلى هذا الـ inbound لا يطابق أي عميل، يتم توجيهه إلى inbound آخر. اختر فرعًا أدناه وسيتم ملء حقول التوجيه (SNI / ALPN / Path / xver) تلقائيًا من نقل الفرع — في الغالب لا تحتاج إلى أي تعديل إضافي. يجب أن يستمع كل فرع على 127.0.0.1 مع security=none.",
"empty": "لا توجد fallbacks بعد",
"add": "إضافة fallback",
"pickInbound": "اختر inbound",
"matchAny": "أي",
"rederive": "إعادة الملء من الفرع",
"rederived": "تم إعادة الملء من الفرع",
"editAdvanced": "تحرير حقول التوجيه",
"hideAdvanced": "إخفاء المتقدم",
"quickAddAll": "إضافة سريعة لكل الـ inbounds المؤهلة",
"quickAdded": "تمت إضافة {n} fallback",
"quickAddedNone": "لا توجد inbounds جديدة مؤهلة للإضافة",
"routesWhen": "يوجَّه عندما",
"defaultCatchAll": "افتراضي — يلتقط أي شيء آخر"
},
"protocol": "بروتوكول",
"port": "بورت",

View file

@ -250,6 +250,23 @@
"node": "Node",
"deployTo": "Deploy to",
"localPanel": "Local panel",
"fallbacks": {
"title": "Fallbacks",
"help": "When a connection on this inbound does not match any client, route it to another inbound. Pick a child below and the routing fields (SNI / ALPN / path / xver) auto-fill from its transport — most setups need no further tweaking. Each child should listen on 127.0.0.1 with security=none.",
"empty": "No fallbacks yet",
"add": "Add fallback",
"pickInbound": "Pick an inbound",
"matchAny": "any",
"rederive": "Re-fill from child",
"rederived": "Re-filled from child",
"editAdvanced": "Edit routing fields",
"hideAdvanced": "Hide advanced",
"quickAddAll": "Quick add all eligible",
"quickAdded": "Added {n} fallback(s)",
"quickAddedNone": "No new eligible inbounds to add",
"routesWhen": "Routes when",
"defaultCatchAll": "Default — catches anything else"
},
"protocol": "Protocol",
"port": "Port",
"portMap": "Port Mapping",

View file

@ -250,11 +250,22 @@
"node": "Nodo",
"deployTo": "Desplegar en",
"localPanel": "Panel local",
"portFallback": {
"title": "Inbounds de respaldo",
"help": "Selecciona los inbounds que deben recibir el tráfico que este inbound VLESS-TLS no coincida. Cada hijo debe escuchar en 127.0.0.1 para recibir las conexiones reenviadas.",
"child": "Inbound",
"path": "Ruta"
"fallbacks": {
"title": "Fallbacks",
"help": "Cuando una conexión en este inbound no coincide con ningún cliente, redirígela a otro inbound. Elige un hijo abajo y los campos de enrutamiento (SNI / ALPN / Path / xver) se rellenan automáticamente desde su transporte; la mayoría de las configuraciones no necesitan más ajustes. Cada hijo debe escuchar en 127.0.0.1 con security=none.",
"empty": "Aún no hay fallbacks",
"add": "Añadir fallback",
"pickInbound": "Selecciona un inbound",
"matchAny": "cualquiera",
"rederive": "Rellenar desde el hijo",
"rederived": "Rellenado desde el hijo",
"editAdvanced": "Editar campos de enrutamiento",
"hideAdvanced": "Ocultar avanzado",
"quickAddAll": "Añadir todos los elegibles",
"quickAdded": "Se añadieron {n} fallback(s)",
"quickAddedNone": "No hay nuevos inbounds elegibles",
"routesWhen": "Enruta cuando",
"defaultCatchAll": "Por defecto — captura cualquier otra cosa"
},
"protocol": "Protocolo",
"port": "Puerto",

View file

@ -250,11 +250,22 @@
"node": "نود",
"deployTo": "استقرار روی",
"localPanel": "پنل لوکال",
"portFallback": {
"title": "فرزندان فال‌بک",
"help": "اینباندهایی را که باید ترافیک نامطابق این اینباند VLESS-TLS را دریافت کنند انتخاب کنید. هر فرزند باید روی 127.0.0.1 گوش دهد تا اتصالات ارجاع‌شده را بپذیرد.",
"child": "اینباند",
"path": "مسیر"
"fallbacks": {
"title": "فال‌بک‌ها",
"help": "وقتی اتصالی روی این اینباند با هیچ کلاینتی تطبیق پیدا نمی‌کند، به یک اینباند دیگر ارجاع داده می‌شود. یک فرزند انتخاب کنید، فیلدهای مسیریابی (SNI / ALPN / Path / xver) خودکار از روی transport آن پر می‌شود — برای بیشتر تنظیمات نیازی به ویرایش نیست. هر فرزند باید روی 127.0.0.1 با security=none گوش بدهد.",
"empty": "هنوز فال‌بکی اضافه نشده",
"add": "افزودن فال‌بک",
"pickInbound": "یک اینباند انتخاب کنید",
"matchAny": "همه",
"rederive": "پر کردن مجدد از فرزند",
"rederived": "از فرزند پر شد",
"editAdvanced": "ویرایش فیلدهای مسیریابی",
"hideAdvanced": "بستن پیشرفته",
"quickAddAll": "افزودن سریع همه‌ی موارد واجد شرایط",
"quickAdded": "{n} فال‌بک افزوده شد",
"quickAddedNone": "اینباند جدیدی برای افزودن وجود ندارد",
"routesWhen": "هدایت می‌شود وقتی",
"defaultCatchAll": "پیش‌فرض — همه‌ی موارد دیگر را می‌گیرد"
},
"protocol": "پروتکل",
"port": "پورت",

View file

@ -250,11 +250,22 @@
"node": "Node",
"deployTo": "Terapkan ke",
"localPanel": "Panel lokal",
"portFallback": {
"title": "Inbound cadangan",
"help": "Pilih inbound yang harus menangkap lalu lintas yang tidak cocok pada inbound VLESS-TLS ini. Setiap anak harus mendengarkan di 127.0.0.1 untuk menerima koneksi yang diteruskan.",
"child": "Inbound",
"path": "Path"
"fallbacks": {
"title": "Fallback",
"help": "Saat koneksi pada inbound ini tidak cocok dengan client mana pun, arahkan ke inbound lain. Pilih child di bawah dan field routing (SNI / ALPN / Path / xver) terisi otomatis dari transport-nya — sebagian besar konfigurasi tidak perlu disesuaikan lagi. Setiap child harus listen di 127.0.0.1 dengan security=none.",
"empty": "Belum ada fallback",
"add": "Tambah fallback",
"pickInbound": "Pilih inbound",
"matchAny": "apa pun",
"rederive": "Isi ulang dari child",
"rederived": "Diisi ulang dari child",
"editAdvanced": "Edit field routing",
"hideAdvanced": "Sembunyikan lanjutan",
"quickAddAll": "Tambah cepat semua yang memenuhi syarat",
"quickAdded": "Menambahkan {n} fallback",
"quickAddedNone": "Tidak ada inbound baru yang memenuhi syarat",
"routesWhen": "Diarahkan ketika",
"defaultCatchAll": "Default — menangkap apa pun lainnya"
},
"protocol": "Protokol",
"port": "Port",

View file

@ -250,11 +250,22 @@
"node": "ノード",
"deployTo": "デプロイ先",
"localPanel": "ローカルパネル",
"portFallback": {
"title": "フォールバック子",
"help": "この VLESS-TLS インバウンドが一致しないトラフィックを受け取るインバウンドを選択してください。各子インバウンドは転送された接続を受信するために 127.0.0.1 でリッスンする必要があります。",
"child": "インバウンド",
"path": "パス"
"fallbacks": {
"title": "フォールバック",
"help": "このインバウンドへの接続がどのクライアントにも一致しない場合、別のインバウンドへルーティングします。下から子インバウンドを選ぶと、ルーティング項目SNI / ALPN / Path / xverはその子のトランスポートから自動的に埋められます — ほとんどの構成で追加の調整は不要です。各子インバウンドは 127.0.0.1 で security=none をリッスンする必要があります。",
"empty": "フォールバックはまだありません",
"add": "フォールバックを追加",
"pickInbound": "インバウンドを選択",
"matchAny": "任意",
"rederive": "子から再取得",
"rederived": "子から再取得しました",
"editAdvanced": "ルーティング項目を編集",
"hideAdvanced": "詳細を隠す",
"quickAddAll": "対象のインバウンドをすべて一括追加",
"quickAdded": "{n} 件のフォールバックを追加しました",
"quickAddedNone": "追加可能な新規インバウンドはありません",
"routesWhen": "次の条件でルーティング",
"defaultCatchAll": "デフォルト — その他すべてを捕捉"
},
"protocol": "プロトコル",
"port": "ポート",

View file

@ -250,11 +250,22 @@
"node": "Nó",
"deployTo": "Implantar em",
"localPanel": "Painel local",
"portFallback": {
"title": "Inbounds de fallback",
"help": "Selecione os inbounds que devem receber o tráfego que este inbound VLESS-TLS não atender. Cada filho precisa escutar em 127.0.0.1 para receber as conexões encaminhadas.",
"child": "Inbound",
"path": "Caminho"
"fallbacks": {
"title": "Fallbacks",
"help": "Quando uma conexão neste inbound não corresponde a nenhum cliente, redirecione-a para outro inbound. Escolha um filho abaixo e os campos de roteamento (SNI / ALPN / Path / xver) são preenchidos automaticamente a partir do transporte dele — a maioria das configurações não precisa de mais ajustes. Cada filho deve escutar em 127.0.0.1 com security=none.",
"empty": "Ainda sem fallbacks",
"add": "Adicionar fallback",
"pickInbound": "Escolha um inbound",
"matchAny": "qualquer",
"rederive": "Preencher a partir do filho",
"rederived": "Preenchido a partir do filho",
"editAdvanced": "Editar campos de roteamento",
"hideAdvanced": "Ocultar avançado",
"quickAddAll": "Adicionar todos os elegíveis",
"quickAdded": "{n} fallback(s) adicionado(s)",
"quickAddedNone": "Nenhum inbound novo elegível para adicionar",
"routesWhen": "Roteia quando",
"defaultCatchAll": "Padrão — captura qualquer outra coisa"
},
"protocol": "Protocolo",
"port": "Porta",

View file

@ -250,11 +250,22 @@
"node": "Узел",
"deployTo": "Развернуть на",
"localPanel": "Локальная панель",
"portFallback": {
"title": "Резервные входящие",
"help": "Выберите входящие, которые должны принимать трафик, не соответствующий этому входящему VLESS-TLS. Каждый дочерний элемент должен слушать на 127.0.0.1 для приёма перенаправленных соединений.",
"child": "Входящий",
"path": "Путь"
"fallbacks": {
"title": "Фолбэки",
"help": "Когда соединение на этом инбаунде не совпадает ни с одним клиентом, оно перенаправляется на другой инбаунд. Выберите дочерний инбаунд ниже — поля маршрутизации (SNI / ALPN / Path / xver) заполнятся автоматически из его транспорта, для большинства конфигураций больше ничего менять не нужно. Каждый дочерний должен слушать на 127.0.0.1 с security=none.",
"empty": "Фолбэков пока нет",
"add": "Добавить фолбэк",
"pickInbound": "Выберите инбаунд",
"matchAny": "любой",
"rederive": "Заполнить из дочернего",
"rederived": "Заполнено из дочернего",
"editAdvanced": "Изменить поля маршрутизации",
"hideAdvanced": "Скрыть расширенные",
"quickAddAll": "Быстро добавить все подходящие",
"quickAdded": "Добавлено {n} фолбэк(ов)",
"quickAddedNone": "Нет новых подходящих инбаундов",
"routesWhen": "Маршрутизирует, когда",
"defaultCatchAll": "По умолчанию — ловит всё остальное"
},
"protocol": "Протокол",
"port": "Порт",

View file

@ -250,11 +250,22 @@
"node": "Düğüm",
"deployTo": "Şuraya dağıt",
"localPanel": "Yerel panel",
"portFallback": {
"title": "Yedek inbound'lar",
"help": "Bu VLESS-TLS inbound'unun eşleşmediği trafiği almasını istediğiniz inbound'ları seçin. Yönlendirilen bağlantıları alabilmek için her alt inbound 127.0.0.1 üzerinde dinlemelidir.",
"child": "Inbound",
"path": "Yol"
"fallbacks": {
"title": "Fallback'ler",
"help": "Bu inbound üzerindeki bir bağlantı hiçbir client ile eşleşmediğinde, başka bir inbound'a yönlendirilir. Aşağıdan bir child seçin; yönlendirme alanları (SNI / ALPN / Path / xver) onun transport'undan otomatik dolar — çoğu kurulum için ek ayar gerekmez. Her child 127.0.0.1 üzerinde security=none ile dinlemelidir.",
"empty": "Henüz fallback yok",
"add": "Fallback ekle",
"pickInbound": "Bir inbound seç",
"matchAny": "herhangi",
"rederive": "Child'dan yeniden doldur",
"rederived": "Child'dan yeniden dolduruldu",
"editAdvanced": "Yönlendirme alanlarını düzenle",
"hideAdvanced": "Gelişmişi gizle",
"quickAddAll": "Uygun olan tümünü hızlı ekle",
"quickAdded": "{n} fallback eklendi",
"quickAddedNone": "Eklenecek yeni uygun inbound yok",
"routesWhen": "Şu durumda yönlendirir",
"defaultCatchAll": "Varsayılan — başka her şeyi yakalar"
},
"protocol": "Protokol",
"port": "Port",

View file

@ -250,11 +250,22 @@
"node": "Вузол",
"deployTo": "Розгорнути на",
"localPanel": "Локальна панель",
"portFallback": {
"title": "Резервні вхідні",
"help": "Виберіть вхідні, які мають отримувати трафік, що не відповідає цьому вхідному VLESS-TLS. Кожен дочірній вхідний повинен слухати на 127.0.0.1 для приймання переадресованих з'єднань.",
"child": "Вхідний",
"path": "Шлях"
"fallbacks": {
"title": "Фолбеки",
"help": "Коли з'єднання на цьому інбаунді не збігається з жодним клієнтом, воно перенаправляється на інший інбаунд. Оберіть дочірній інбаунд нижче — поля маршрутизації (SNI / ALPN / Path / xver) заповняться автоматично з його транспорту; для більшості налаштувань більше нічого змінювати не треба. Кожен дочірній має слухати на 127.0.0.1 з security=none.",
"empty": "Фолбеків поки немає",
"add": "Додати фолбек",
"pickInbound": "Оберіть інбаунд",
"matchAny": "будь-який",
"rederive": "Заповнити з дочірнього",
"rederived": "Заповнено з дочірнього",
"editAdvanced": "Редагувати поля маршрутизації",
"hideAdvanced": "Сховати розширені",
"quickAddAll": "Швидко додати всі придатні",
"quickAdded": "Додано {n} фолбек(ів)",
"quickAddedNone": "Немає нових придатних інбаундів",
"routesWhen": "Маршрутизує, коли",
"defaultCatchAll": "За замовчуванням — ловить усе інше"
},
"protocol": "Протокол",
"port": "Порт",

View file

@ -250,11 +250,22 @@
"node": "Nút",
"deployTo": "Triển khai tới",
"localPanel": "Panel cục bộ",
"portFallback": {
"title": "Inbound dự phòng",
"help": "Chọn các inbound sẽ tiếp nhận lưu lượng mà inbound VLESS-TLS này không khớp. Mỗi inbound con phải lắng nghe trên 127.0.0.1 để nhận các kết nối được chuyển tiếp.",
"child": "Inbound",
"path": "Đường dẫn"
"fallbacks": {
"title": "Fallback",
"help": "Khi một kết nối trên inbound này không khớp với client nào, nó sẽ được chuyển hướng tới inbound khác. Chọn một child bên dưới và các trường định tuyến (SNI / ALPN / Path / xver) sẽ được tự động điền từ transport của child — hầu hết cấu hình không cần chỉnh thêm. Mỗi child nên lắng nghe trên 127.0.0.1 với security=none.",
"empty": "Chưa có fallback nào",
"add": "Thêm fallback",
"pickInbound": "Chọn một inbound",
"matchAny": "bất kỳ",
"rederive": "Điền lại từ child",
"rederived": "Đã điền lại từ child",
"editAdvanced": "Sửa trường định tuyến",
"hideAdvanced": "Ẩn nâng cao",
"quickAddAll": "Thêm nhanh tất cả các inbound đủ điều kiện",
"quickAdded": "Đã thêm {n} fallback",
"quickAddedNone": "Không có inbound mới nào đủ điều kiện",
"routesWhen": "Định tuyến khi",
"defaultCatchAll": "Mặc định — bắt mọi thứ khác"
},
"protocol": "Giao thức",
"port": "Cổng",

View file

@ -250,11 +250,22 @@
"node": "节点",
"deployTo": "部署到",
"localPanel": "本地面板",
"portFallback": {
"title": "回退入站",
"help": "选择当此 VLESS-TLS 入站不匹配时应接收流量的入站。每个子入站必须监听 127.0.0.1 才能接收转发的连接。",
"child": "入站",
"path": "路径"
"fallbacks": {
"title": "回落",
"help": "当此入站的连接未匹配任何客户端时将其路由到另一个入站。在下方选择一个子入站路由字段SNI / ALPN / Path / xver会从子入站的传输方式中自动填充——大多数场景无需再调整。每个子入站应监听 127.0.0.1security=none。",
"empty": "暂无回落",
"add": "添加回落",
"pickInbound": "选择一个入站",
"matchAny": "任意",
"rederive": "从子入站重新填充",
"rederived": "已从子入站重新填充",
"editAdvanced": "编辑路由字段",
"hideAdvanced": "隐藏高级",
"quickAddAll": "一键添加所有可用入站",
"quickAdded": "已添加 {n} 条回落",
"quickAddedNone": "没有可添加的新入站",
"routesWhen": "当满足条件时路由",
"defaultCatchAll": "默认 — 兜底匹配其他所有"
},
"protocol": "协议",
"port": "端口",

View file

@ -250,11 +250,22 @@
"node": "節點",
"deployTo": "部署到",
"localPanel": "本機面板",
"portFallback": {
"title": "回退入站",
"help": "選擇當此 VLESS-TLS 入站未匹配時應接收流量的入站。每個子入站必須監聽 127.0.0.1 才能接收轉發的連線。",
"child": "入站",
"path": "路徑"
"fallbacks": {
"title": "回落",
"help": "當此入站的連線未匹配任何用戶時將其路由到另一個入站。在下方選擇一個子入站路由欄位SNI / ALPN / Path / xver會自動從子入站的傳輸方式填入——大多數情境不需要再調整。每個子入站應監聽 127.0.0.1security=none。",
"empty": "尚未新增回落",
"add": "新增回落",
"pickInbound": "選擇一個入站",
"matchAny": "任何",
"rederive": "從子入站重新填入",
"rederived": "已從子入站重新填入",
"editAdvanced": "編輯路由欄位",
"hideAdvanced": "隱藏進階",
"quickAddAll": "一鍵新增所有符合的入站",
"quickAdded": "已新增 {n} 個回落",
"quickAddedNone": "沒有可新增的新入站",
"routesWhen": "當條件成立時路由",
"defaultCatchAll": "預設 — 兜底匹配其餘"
},
"protocol": "協議",
"port": "埠",