1
0

routing-builder.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. 'use client';
  2. import { useState } from 'react';
  3. import {
  4. buildRoutingJson,
  5. type DomainStrategy,
  6. type RoutingInput,
  7. type RuleNetwork,
  8. type Strategy,
  9. } from '@/lib/xray/routing';
  10. import { ToolFrame } from './tool-frame';
  11. import { TextField, SelectField } from './shared/fields';
  12. import { OutputBlock } from './shared/output-block';
  13. interface BalancerRow {
  14. tag: string;
  15. selector: string;
  16. strategy: Strategy;
  17. fallbackTag: string;
  18. }
  19. interface RuleRow {
  20. domain: string;
  21. ip: string;
  22. port: string;
  23. network: string;
  24. inboundTag: string;
  25. targetKind: 'outbound' | 'balancer';
  26. targetTag: string;
  27. }
  28. const STRATEGIES: readonly Strategy[] = ['random', 'roundRobin', 'leastPing', 'leastLoad'];
  29. const NETWORKS = ['any', 'tcp', 'udp', 'tcp,udp'];
  30. const TARGET_KINDS = ['outbound', 'balancer'];
  31. const DOMAIN_STRATEGIES: readonly DomainStrategy[] = ['AsIs', 'IPIfNonMatch', 'IPOnDemand'];
  32. const DEFAULT_BALANCERS: BalancerRow[] = [
  33. { tag: 'balancer', selector: 'proxy', strategy: 'leastPing', fallbackTag: '' },
  34. ];
  35. const DEFAULT_RULES: RuleRow[] = [
  36. { domain: 'geosite:category-ads-all', ip: '', port: '', network: 'any', inboundTag: '', targetKind: 'outbound', targetTag: 'block' },
  37. { domain: '', ip: 'geoip:private', port: '', network: 'any', inboundTag: '', targetKind: 'outbound', targetTag: 'direct' },
  38. ];
  39. function list(s: string): string[] {
  40. return s
  41. .split(',')
  42. .map((x) => x.trim())
  43. .filter(Boolean);
  44. }
  45. const addBtn =
  46. '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';
  47. export function RoutingBuilder() {
  48. const [domainStrategy, setDomainStrategy] = useState<DomainStrategy>('IPIfNonMatch');
  49. const [balancers, setBalancers] = useState<BalancerRow[]>(DEFAULT_BALANCERS);
  50. const [rules, setRules] = useState<RuleRow[]>(DEFAULT_RULES);
  51. function patchBalancer(i: number, patch: Partial<BalancerRow>) {
  52. setBalancers((prev) => prev.map((b, j) => (i === j ? { ...b, ...patch } : b)));
  53. }
  54. function patchRule(i: number, patch: Partial<RuleRow>) {
  55. setRules((prev) => prev.map((r, j) => (i === j ? { ...r, ...patch } : r)));
  56. }
  57. const input: RoutingInput = {
  58. domainStrategy,
  59. balancers: balancers
  60. .filter((b) => b.tag.trim())
  61. .map((b) => ({
  62. tag: b.tag.trim(),
  63. selector: list(b.selector),
  64. strategy: b.strategy,
  65. fallbackTag: b.fallbackTag.trim() || undefined,
  66. })),
  67. rules: rules
  68. .filter((r) => r.targetTag.trim())
  69. .map((r) => ({
  70. domain: list(r.domain),
  71. ip: list(r.ip),
  72. port: r.port.trim() || undefined,
  73. network: r.network === 'any' ? undefined : (r.network as RuleNetwork),
  74. inboundTag: list(r.inboundTag),
  75. target: { kind: r.targetKind, tag: r.targetTag.trim() },
  76. })),
  77. };
  78. function reset() {
  79. setDomainStrategy('IPIfNonMatch');
  80. setBalancers(DEFAULT_BALANCERS);
  81. setRules(DEFAULT_RULES);
  82. }
  83. return (
  84. <ToolFrame
  85. title="Balancer & routing builder"
  86. description="Compose Xray balancers and routing rules, then copy the routing block (with a matching observatory for leastPing/leastLoad)."
  87. onReset={reset}
  88. >
  89. <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
  90. <SelectField
  91. label="Domain strategy"
  92. value={domainStrategy}
  93. onChange={(v) => setDomainStrategy(v as DomainStrategy)}
  94. options={DOMAIN_STRATEGIES}
  95. />
  96. </div>
  97. <div className="mt-5 flex items-center justify-between">
  98. <h4 className="text-sm font-semibold">Balancers</h4>
  99. <button
  100. type="button"
  101. className={addBtn}
  102. onClick={() =>
  103. setBalancers((p) => [...p, { tag: '', selector: '', strategy: 'random', fallbackTag: '' }])
  104. }
  105. >
  106. Add balancer
  107. </button>
  108. </div>
  109. <div className="mt-2 flex flex-col gap-3">
  110. {balancers.map((b, i) => (
  111. <div key={i} className="rounded-xl border p-3">
  112. <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
  113. <TextField label="Tag" value={b.tag} onChange={(v) => patchBalancer(i, { tag: v })} />
  114. <TextField
  115. label="Selector (comma-separated prefixes)"
  116. value={b.selector}
  117. onChange={(v) => patchBalancer(i, { selector: v })}
  118. />
  119. <SelectField
  120. label="Strategy"
  121. value={b.strategy}
  122. onChange={(v) => patchBalancer(i, { strategy: v as Strategy })}
  123. options={STRATEGIES}
  124. />
  125. <TextField
  126. label="Fallback tag"
  127. value={b.fallbackTag}
  128. onChange={(v) => patchBalancer(i, { fallbackTag: v })}
  129. placeholder="optional"
  130. />
  131. </div>
  132. <div className="mt-2 flex justify-end">
  133. <button
  134. type="button"
  135. className={addBtn}
  136. onClick={() => setBalancers((p) => p.filter((_, j) => j !== i))}
  137. >
  138. Remove
  139. </button>
  140. </div>
  141. </div>
  142. ))}
  143. </div>
  144. <div className="mt-5 flex items-center justify-between">
  145. <h4 className="text-sm font-semibold">Rules</h4>
  146. <button
  147. type="button"
  148. className={addBtn}
  149. onClick={() =>
  150. setRules((p) => [
  151. ...p,
  152. { domain: '', ip: '', port: '', network: 'any', inboundTag: '', targetKind: 'outbound', targetTag: '' },
  153. ])
  154. }
  155. >
  156. Add rule
  157. </button>
  158. </div>
  159. <div className="mt-2 flex flex-col gap-3">
  160. {rules.map((r, i) => (
  161. <div key={i} className="rounded-xl border p-3">
  162. <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
  163. <TextField label="Domain (comma)" value={r.domain} onChange={(v) => patchRule(i, { domain: v })} placeholder="geosite:google, example.com" />
  164. <TextField label="IP (comma)" value={r.ip} onChange={(v) => patchRule(i, { ip: v })} placeholder="geoip:cn, 1.1.1.1" />
  165. <TextField label="Port" value={r.port} onChange={(v) => patchRule(i, { port: v })} placeholder="443 or 1000-2000" />
  166. <SelectField label="Network" value={r.network} onChange={(v) => patchRule(i, { network: v })} options={NETWORKS} />
  167. <TextField label="Inbound tag (comma)" value={r.inboundTag} onChange={(v) => patchRule(i, { inboundTag: v })} placeholder="optional" />
  168. <SelectField label="Target kind" value={r.targetKind} onChange={(v) => patchRule(i, { targetKind: v as 'outbound' | 'balancer' })} options={TARGET_KINDS} />
  169. <TextField label="Target tag" value={r.targetTag} onChange={(v) => patchRule(i, { targetTag: v })} />
  170. </div>
  171. <div className="mt-2 flex justify-end">
  172. <button
  173. type="button"
  174. className={addBtn}
  175. onClick={() => setRules((p) => p.filter((_, j) => j !== i))}
  176. >
  177. Remove
  178. </button>
  179. </div>
  180. </div>
  181. ))}
  182. </div>
  183. <div className="mt-4">
  184. <OutputBlock label="Routing block (Xray JSON)" value={buildRoutingJson(input)} />
  185. </div>
  186. </ToolFrame>
  187. );
  188. }