routing-builder.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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. {
  37. domain: 'geosite:category-ads-all',
  38. ip: '',
  39. port: '',
  40. network: 'any',
  41. inboundTag: '',
  42. targetKind: 'outbound',
  43. targetTag: 'block',
  44. },
  45. {
  46. domain: '',
  47. ip: 'geoip:private',
  48. port: '',
  49. network: 'any',
  50. inboundTag: '',
  51. targetKind: 'outbound',
  52. targetTag: 'direct',
  53. },
  54. ];
  55. function list(s: string): string[] {
  56. return s
  57. .split(',')
  58. .map((x) => x.trim())
  59. .filter(Boolean);
  60. }
  61. const addBtn =
  62. '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';
  63. export function RoutingBuilder() {
  64. const [domainStrategy, setDomainStrategy] = useState<DomainStrategy>('IPIfNonMatch');
  65. const [balancers, setBalancers] = useState<BalancerRow[]>(DEFAULT_BALANCERS);
  66. const [rules, setRules] = useState<RuleRow[]>(DEFAULT_RULES);
  67. function patchBalancer(i: number, patch: Partial<BalancerRow>) {
  68. setBalancers((prev) => prev.map((b, j) => (i === j ? { ...b, ...patch } : b)));
  69. }
  70. function patchRule(i: number, patch: Partial<RuleRow>) {
  71. setRules((prev) => prev.map((r, j) => (i === j ? { ...r, ...patch } : r)));
  72. }
  73. const input: RoutingInput = {
  74. domainStrategy,
  75. balancers: balancers
  76. .filter((b) => b.tag.trim())
  77. .map((b) => ({
  78. tag: b.tag.trim(),
  79. selector: list(b.selector),
  80. strategy: b.strategy,
  81. fallbackTag: b.fallbackTag.trim() || undefined,
  82. })),
  83. rules: rules
  84. .filter((r) => r.targetTag.trim())
  85. .map((r) => ({
  86. domain: list(r.domain),
  87. ip: list(r.ip),
  88. port: r.port.trim() || undefined,
  89. network: r.network === 'any' ? undefined : (r.network as RuleNetwork),
  90. inboundTag: list(r.inboundTag),
  91. target: { kind: r.targetKind, tag: r.targetTag.trim() },
  92. })),
  93. };
  94. function reset() {
  95. setDomainStrategy('IPIfNonMatch');
  96. setBalancers(DEFAULT_BALANCERS);
  97. setRules(DEFAULT_RULES);
  98. }
  99. return (
  100. <ToolFrame
  101. title="Balancer & routing builder"
  102. description="Compose Xray balancers and routing rules, then copy the routing block (with a matching observatory for leastPing/leastLoad)."
  103. onReset={reset}
  104. >
  105. <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
  106. <SelectField
  107. label="Domain strategy"
  108. value={domainStrategy}
  109. onChange={(v) => setDomainStrategy(v as DomainStrategy)}
  110. options={DOMAIN_STRATEGIES}
  111. />
  112. </div>
  113. <div className="mt-5 flex items-center justify-between">
  114. <h4 className="text-sm font-semibold">Balancers</h4>
  115. <button
  116. type="button"
  117. className={addBtn}
  118. onClick={() =>
  119. setBalancers((p) => [
  120. ...p,
  121. { tag: '', selector: '', strategy: 'random', fallbackTag: '' },
  122. ])
  123. }
  124. >
  125. Add balancer
  126. </button>
  127. </div>
  128. <div className="mt-2 flex flex-col gap-3">
  129. {balancers.map((b, i) => (
  130. <div key={i} className="rounded-xl border p-3">
  131. <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
  132. <TextField label="Tag" value={b.tag} onChange={(v) => patchBalancer(i, { tag: v })} />
  133. <TextField
  134. label="Selector (comma-separated prefixes)"
  135. value={b.selector}
  136. onChange={(v) => patchBalancer(i, { selector: v })}
  137. />
  138. <SelectField
  139. label="Strategy"
  140. value={b.strategy}
  141. onChange={(v) => patchBalancer(i, { strategy: v as Strategy })}
  142. options={STRATEGIES}
  143. />
  144. <TextField
  145. label="Fallback tag"
  146. value={b.fallbackTag}
  147. onChange={(v) => patchBalancer(i, { fallbackTag: v })}
  148. placeholder="optional"
  149. />
  150. </div>
  151. <div className="mt-2 flex justify-end">
  152. <button
  153. type="button"
  154. className={addBtn}
  155. onClick={() => setBalancers((p) => p.filter((_, j) => j !== i))}
  156. >
  157. Remove
  158. </button>
  159. </div>
  160. </div>
  161. ))}
  162. </div>
  163. <div className="mt-5 flex items-center justify-between">
  164. <h4 className="text-sm font-semibold">Rules</h4>
  165. <button
  166. type="button"
  167. className={addBtn}
  168. onClick={() =>
  169. setRules((p) => [
  170. ...p,
  171. {
  172. domain: '',
  173. ip: '',
  174. port: '',
  175. network: 'any',
  176. inboundTag: '',
  177. targetKind: 'outbound',
  178. targetTag: '',
  179. },
  180. ])
  181. }
  182. >
  183. Add rule
  184. </button>
  185. </div>
  186. <div className="mt-2 flex flex-col gap-3">
  187. {rules.map((r, i) => (
  188. <div key={i} className="rounded-xl border p-3">
  189. <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
  190. <TextField
  191. label="Domain (comma)"
  192. value={r.domain}
  193. onChange={(v) => patchRule(i, { domain: v })}
  194. placeholder="geosite:google, example.com"
  195. />
  196. <TextField
  197. label="IP (comma)"
  198. value={r.ip}
  199. onChange={(v) => patchRule(i, { ip: v })}
  200. placeholder="geoip:cn, 1.1.1.1"
  201. />
  202. <TextField
  203. label="Port"
  204. value={r.port}
  205. onChange={(v) => patchRule(i, { port: v })}
  206. placeholder="443 or 1000-2000"
  207. />
  208. <SelectField
  209. label="Network"
  210. value={r.network}
  211. onChange={(v) => patchRule(i, { network: v })}
  212. options={NETWORKS}
  213. />
  214. <TextField
  215. label="Inbound tag (comma)"
  216. value={r.inboundTag}
  217. onChange={(v) => patchRule(i, { inboundTag: v })}
  218. placeholder="optional"
  219. />
  220. <SelectField
  221. label="Target kind"
  222. value={r.targetKind}
  223. onChange={(v) => patchRule(i, { targetKind: v as 'outbound' | 'balancer' })}
  224. options={TARGET_KINDS}
  225. />
  226. <TextField
  227. label="Target tag"
  228. value={r.targetTag}
  229. onChange={(v) => patchRule(i, { targetTag: v })}
  230. />
  231. </div>
  232. <div className="mt-2 flex justify-end">
  233. <button
  234. type="button"
  235. className={addBtn}
  236. onClick={() => setRules((p) => p.filter((_, j) => j !== i))}
  237. >
  238. Remove
  239. </button>
  240. </div>
  241. </div>
  242. ))}
  243. </div>
  244. <div className="mt-4">
  245. <OutputBlock label="Routing block (Xray JSON)" value={buildRoutingJson(input)} />
  246. </div>
  247. </ToolFrame>
  248. );
  249. }