| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- '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>
- );
- }
|