routing.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. // Pure builders for an Xray routing block — balancers, routing rules, and the
  2. // observatory/burstObservatory health monitors — matching 3x-ui's schemas
  3. // (frontend schemas/routing.ts, observatory.ts, xray.ts):
  4. // - balancers live under `routing.balancers[]`; rules under `routing.rules[]`.
  5. // - `observatory` / `burstObservatory` are TOP-LEVEL (siblings of routing).
  6. // - leastPing pairs with observatory; leastLoad with burstObservatory.
  7. // No React/DOM imports — unit-tested in Node.
  8. export type Strategy = 'random' | 'roundRobin' | 'leastPing' | 'leastLoad';
  9. export type RuleNetwork = 'tcp' | 'udp' | 'tcp,udp';
  10. export type DomainStrategy = 'AsIs' | 'IPIfNonMatch' | 'IPOnDemand';
  11. export interface BalancerInput {
  12. tag: string;
  13. selector: string[];
  14. strategy: Strategy;
  15. fallbackTag?: string;
  16. }
  17. export interface RuleTarget {
  18. kind: 'outbound' | 'balancer';
  19. tag: string;
  20. }
  21. export interface RuleInput {
  22. domain?: string[];
  23. ip?: string[];
  24. port?: string; // "443", "1000-2000", or "443,8443"
  25. network?: RuleNetwork;
  26. protocol?: string[]; // http | tls | quic | bittorrent
  27. inboundTag?: string[];
  28. source?: string[]; // -> sourceIP
  29. target: RuleTarget;
  30. ruleTag?: string;
  31. }
  32. export interface ObservatoryInput {
  33. mode: 'observatory' | 'burst';
  34. subjectSelector: string[];
  35. probeURL?: string; // observatory
  36. probeInterval?: string; // observatory
  37. destination?: string; // burst pingConfig
  38. interval?: string; // burst pingConfig
  39. }
  40. export interface RoutingInput {
  41. domainStrategy?: DomainStrategy;
  42. balancers: BalancerInput[];
  43. rules: RuleInput[];
  44. observatory?: ObservatoryInput;
  45. }
  46. const DEFAULT_PROBE_URL = 'https://www.google.com/generate_204';
  47. const DEFAULT_PROBE_INTERVAL = '1m';
  48. export function buildBalancer(b: BalancerInput): Record<string, unknown> {
  49. const out: Record<string, unknown> = {
  50. tag: b.tag,
  51. selector: b.selector,
  52. strategy: { type: b.strategy },
  53. };
  54. if (b.fallbackTag) out.fallbackTag = b.fallbackTag;
  55. return out;
  56. }
  57. export function buildRule(r: RuleInput): Record<string, unknown> {
  58. const out: Record<string, unknown> = { type: 'field' };
  59. if (r.domain?.length) out.domain = r.domain;
  60. if (r.ip?.length) out.ip = r.ip;
  61. if (r.port) out.port = r.port;
  62. if (r.network) out.network = r.network;
  63. if (r.protocol?.length) out.protocol = r.protocol;
  64. if (r.inboundTag?.length) out.inboundTag = r.inboundTag;
  65. if (r.source?.length) out.sourceIP = r.source;
  66. if (r.ruleTag) out.ruleTag = r.ruleTag;
  67. if (r.target.kind === 'balancer') out.balancerTag = r.target.tag;
  68. else out.outboundTag = r.target.tag;
  69. return out;
  70. }
  71. export function buildObservatory(o: ObservatoryInput): Record<string, unknown> {
  72. if (o.mode === 'observatory') {
  73. return {
  74. observatory: {
  75. subjectSelector: o.subjectSelector,
  76. probeURL: o.probeURL || DEFAULT_PROBE_URL,
  77. probeInterval: o.probeInterval || DEFAULT_PROBE_INTERVAL,
  78. enableConcurrency: true,
  79. },
  80. };
  81. }
  82. return {
  83. burstObservatory: {
  84. subjectSelector: o.subjectSelector,
  85. pingConfig: {
  86. destination: o.destination || DEFAULT_PROBE_URL,
  87. interval: o.interval || DEFAULT_PROBE_INTERVAL,
  88. timeout: '5s',
  89. sampling: 2,
  90. httpMethod: 'HEAD',
  91. },
  92. },
  93. };
  94. }
  95. function uniqueSelectors(balancers: BalancerInput[]): string[] {
  96. return [...new Set(balancers.flatMap((b) => b.selector))];
  97. }
  98. export function buildRouting(input: RoutingInput): Record<string, unknown> {
  99. const routing: Record<string, unknown> = {};
  100. if (input.domainStrategy) routing.domainStrategy = input.domainStrategy;
  101. routing.rules = input.rules.map(buildRule);
  102. routing.balancers = input.balancers.map(buildBalancer);
  103. const out: Record<string, unknown> = { routing };
  104. // Latency-aware strategies need a health monitor. Honor an explicit one;
  105. // otherwise scaffold the matching monitor (observatory for leastPing,
  106. // burstObservatory for leastLoad) selecting the balancers' own selectors.
  107. if (input.observatory) {
  108. Object.assign(out, buildObservatory(input.observatory));
  109. } else if (input.balancers.some((b) => b.strategy === 'leastLoad')) {
  110. Object.assign(out, buildObservatory({ mode: 'burst', subjectSelector: uniqueSelectors(input.balancers) }));
  111. } else if (input.balancers.some((b) => b.strategy === 'leastPing')) {
  112. Object.assign(
  113. out,
  114. buildObservatory({ mode: 'observatory', subjectSelector: uniqueSelectors(input.balancers) }),
  115. );
  116. }
  117. return out;
  118. }
  119. export function buildRoutingJson(input: RoutingInput): string {
  120. return JSON.stringify(buildRouting(input), null, 2);
  121. }