Files
3x-ui-web-src/docs/components/tools/routing-builder.tsx
T
Sanaei f727d04f65
CI / go-test (push) Canceled after 0s
CI / postgres-durable-first (push) Canceled after 0s
CI / codegen (push) Canceled after 0s
CI / govulncheck (push) Canceled after 0s
CI / race (push) Canceled after 0s
CI / fuzz-smoke (push) Canceled after 0s
CI / golangci (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CodeQL Advanced / Analyze (go) (push) Canceled after 0s
CodeQL Advanced / Analyze (actions) (push) Canceled after 0s
CodeQL Advanced / Analyze (javascript-typescript) (push) Canceled after 0s
Docs CI / build (push) Canceled after 0s
Docs Deploy (GitHub Pages) / build (push) Canceled after 0s
Release 3X-UI / build (386) (push) Canceled after 0s
Release 3X-UI / build (amd64) (push) Canceled after 0s
Release 3X-UI / build (arm64) (push) Canceled after 0s
Release 3X-UI / build (armv5) (push) Canceled after 0s
Release 3X-UI / build (armv6) (push) Canceled after 0s
Release 3X-UI / build (armv7) (push) Canceled after 0s
Release 3X-UI / build (s390x) (push) Canceled after 0s
Release 3X-UI / Build for Windows (amd64) (push) Canceled after 0s
Deploy Smoke Tests / noninteractive-install (ubuntu-24.04-arm) (push) Canceled after 0s
Deploy Smoke Tests / noninteractive-install (ubuntu-latest) (push) Canceled after 0s
Deploy Smoke Tests / release-tag-install (ubuntu-24.04-arm) (push) Canceled after 0s
Deploy Smoke Tests / release-tag-install (ubuntu-latest) (push) Canceled after 0s
Docs Deploy (GitHub Pages) / deploy (push) Canceled after 0s
Release 3X-UI / Publish rolling dev release (push) Canceled after 0s
v3.7.0
2026-08-24 15:07:15 +02:00

265 lines
8.0 KiB
TypeScript

'use client';
import { useState } from 'react';
import {
buildRoutingJson,
type DomainStrategy,
type RoutingInput,
type RuleNetwork,
type Strategy,
} from '@/lib/xray/routing';
import { ToolFrame } from './tool-frame';
import { TextField, SelectField } from './shared/fields';
import { OutputBlock } from './shared/output-block';
interface BalancerRow {
tag: string;
selector: string;
strategy: Strategy;
fallbackTag: string;
}
interface RuleRow {
domain: string;
ip: string;
port: string;
network: string;
inboundTag: string;
targetKind: 'outbound' | 'balancer';
targetTag: string;
}
const STRATEGIES: readonly Strategy[] = ['random', 'roundRobin', 'leastPing', 'leastLoad'];
const NETWORKS = ['any', 'tcp', 'udp', 'tcp,udp'];
const TARGET_KINDS = ['outbound', 'balancer'];
const DOMAIN_STRATEGIES: readonly DomainStrategy[] = ['AsIs', 'IPIfNonMatch', 'IPOnDemand'];
const DEFAULT_BALANCERS: BalancerRow[] = [
{ tag: 'balancer', selector: 'proxy', strategy: 'leastPing', fallbackTag: '' },
];
const DEFAULT_RULES: RuleRow[] = [
{
domain: 'geosite:category-ads-all',
ip: '',
port: '',
network: 'any',
inboundTag: '',
targetKind: 'outbound',
targetTag: 'block',
},
{
domain: '',
ip: 'geoip:private',
port: '',
network: 'any',
inboundTag: '',
targetKind: 'outbound',
targetTag: 'direct',
},
];
function list(s: string): string[] {
return s
.split(',')
.map((x) => x.trim())
.filter(Boolean);
}
const addBtn =
'inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground';
export function RoutingBuilder() {
const [domainStrategy, setDomainStrategy] = useState<DomainStrategy>('IPIfNonMatch');
const [balancers, setBalancers] = useState<BalancerRow[]>(DEFAULT_BALANCERS);
const [rules, setRules] = useState<RuleRow[]>(DEFAULT_RULES);
function patchBalancer(i: number, patch: Partial<BalancerRow>) {
setBalancers((prev) => prev.map((b, j) => (i === j ? { ...b, ...patch } : b)));
}
function patchRule(i: number, patch: Partial<RuleRow>) {
setRules((prev) => prev.map((r, j) => (i === j ? { ...r, ...patch } : r)));
}
const input: RoutingInput = {
domainStrategy,
balancers: balancers
.filter((b) => b.tag.trim())
.map((b) => ({
tag: b.tag.trim(),
selector: list(b.selector),
strategy: b.strategy,
fallbackTag: b.fallbackTag.trim() || undefined,
})),
rules: rules
.filter((r) => r.targetTag.trim())
.map((r) => ({
domain: list(r.domain),
ip: list(r.ip),
port: r.port.trim() || undefined,
network: r.network === 'any' ? undefined : (r.network as RuleNetwork),
inboundTag: list(r.inboundTag),
target: { kind: r.targetKind, tag: r.targetTag.trim() },
})),
};
function reset() {
setDomainStrategy('IPIfNonMatch');
setBalancers(DEFAULT_BALANCERS);
setRules(DEFAULT_RULES);
}
return (
<ToolFrame
title="Balancer & routing builder"
description="Compose Xray balancers and routing rules, then copy the routing block (with a matching observatory for leastPing/leastLoad)."
onReset={reset}
>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<SelectField
label="Domain strategy"
value={domainStrategy}
onChange={(v) => setDomainStrategy(v as DomainStrategy)}
options={DOMAIN_STRATEGIES}
/>
</div>
<div className="mt-5 flex items-center justify-between">
<h4 className="text-sm font-semibold">Balancers</h4>
<button
type="button"
className={addBtn}
onClick={() =>
setBalancers((p) => [
...p,
{ tag: '', selector: '', strategy: 'random', fallbackTag: '' },
])
}
>
Add balancer
</button>
</div>
<div className="mt-2 flex flex-col gap-3">
{balancers.map((b, i) => (
<div key={i} className="rounded-xl border p-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<TextField label="Tag" value={b.tag} onChange={(v) => patchBalancer(i, { tag: v })} />
<TextField
label="Selector (comma-separated prefixes)"
value={b.selector}
onChange={(v) => patchBalancer(i, { selector: v })}
/>
<SelectField
label="Strategy"
value={b.strategy}
onChange={(v) => patchBalancer(i, { strategy: v as Strategy })}
options={STRATEGIES}
/>
<TextField
label="Fallback tag"
value={b.fallbackTag}
onChange={(v) => patchBalancer(i, { fallbackTag: v })}
placeholder="optional"
/>
</div>
<div className="mt-2 flex justify-end">
<button
type="button"
className={addBtn}
onClick={() => setBalancers((p) => p.filter((_, j) => j !== i))}
>
Remove
</button>
</div>
</div>
))}
</div>
<div className="mt-5 flex items-center justify-between">
<h4 className="text-sm font-semibold">Rules</h4>
<button
type="button"
className={addBtn}
onClick={() =>
setRules((p) => [
...p,
{
domain: '',
ip: '',
port: '',
network: 'any',
inboundTag: '',
targetKind: 'outbound',
targetTag: '',
},
])
}
>
Add rule
</button>
</div>
<div className="mt-2 flex flex-col gap-3">
{rules.map((r, i) => (
<div key={i} className="rounded-xl border p-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<TextField
label="Domain (comma)"
value={r.domain}
onChange={(v) => patchRule(i, { domain: v })}
placeholder="geosite:google, example.com"
/>
<TextField
label="IP (comma)"
value={r.ip}
onChange={(v) => patchRule(i, { ip: v })}
placeholder="geoip:cn, 1.1.1.1"
/>
<TextField
label="Port"
value={r.port}
onChange={(v) => patchRule(i, { port: v })}
placeholder="443 or 1000-2000"
/>
<SelectField
label="Network"
value={r.network}
onChange={(v) => patchRule(i, { network: v })}
options={NETWORKS}
/>
<TextField
label="Inbound tag (comma)"
value={r.inboundTag}
onChange={(v) => patchRule(i, { inboundTag: v })}
placeholder="optional"
/>
<SelectField
label="Target kind"
value={r.targetKind}
onChange={(v) => patchRule(i, { targetKind: v as 'outbound' | 'balancer' })}
options={TARGET_KINDS}
/>
<TextField
label="Target tag"
value={r.targetTag}
onChange={(v) => patchRule(i, { targetTag: v })}
/>
</div>
<div className="mt-2 flex justify-end">
<button
type="button"
className={addBtn}
onClick={() => setRules((p) => p.filter((_, j) => j !== i))}
>
Remove
</button>
</div>
</div>
))}
</div>
<div className="mt-4">
<OutputBlock label="Routing block (Xray JSON)" value={buildRoutingJson(input)} />
</div>
</ToolFrame>
);
}