| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- import type { XraySettingsValue } from '@/hooks/useXraySetting';
- import type { BalancerObject, RuleObject } from '@/schemas/routing';
- import { syncObservatories } from './balancers/balancer-helpers';
- /**
- * Reference cleanup for the Xray-config blob: when an outbound or balancer is
- * deleted, routing rules and balancers that point at it must be repaired in the
- * same edit, or the saved config breaks the core (a dangling balancerTag stops
- * Router.Init; a dangling outboundTag black-holes matched traffic).
- *
- * Keep/drop a rule by its destination: after the deletion, a rule that still has
- * an outboundTag or balancerTag is kept (the dead reference is dropped); a rule
- * left with neither is removed, since a destination-less rule black-holes the
- * traffic it matches. Deleting an outbound cascades: if it empties a balancer's
- * selector, that balancer is removed too, and its rules are repaired the same way.
- */
- export type RuleFate = 'removed' | 'modified';
- export interface RuleImpact {
- index: number;
- label: string;
- fate: RuleFate;
- keeps?: string;
- }
- export interface BalancerImpact {
- tag: string;
- reason: 'selectorEmptied';
- }
- export interface DeletionImpact {
- rules: RuleImpact[];
- balancers: BalancerImpact[];
- observatory: boolean;
- burst: boolean;
- }
- const emptyImpact = (): DeletionImpact => ({ rules: [], balancers: [], observatory: false, burst: false });
- function ruleList(tt: XraySettingsValue): RuleObject[] {
- const r = tt.routing?.rules;
- return Array.isArray(r) ? r : [];
- }
- function balancerList(tt: XraySettingsValue): BalancerObject[] {
- const b = tt.routing?.balancers;
- return Array.isArray(b) ? b : [];
- }
- function outboundTagAt(tt: XraySettingsValue, index: number): string {
- const o = tt.outbounds?.[index];
- return typeof o?.tag === 'string' ? o.tag : '';
- }
- function balancerTagAt(tt: XraySettingsValue, index: number): string {
- const b = balancerList(tt)[index];
- return typeof b?.tag === 'string' ? b.tag : '';
- }
- function ruleLabel(rule: RuleObject, index: number): string {
- const tag = typeof rule.ruleTag === 'string' ? rule.ruleTag.trim() : '';
- return tag || `#${index + 1}`;
- }
- /** Balancers whose selector is left empty once `removedOutbounds` are gone. */
- function balancersEmptiedBy(tt: XraySettingsValue, removedOutbounds: Set<string>): string[] {
- if (removedOutbounds.size === 0) return [];
- const emptied: string[] = [];
- for (const b of balancerList(tt)) {
- const tag = typeof b?.tag === 'string' ? b.tag : '';
- if (tag === '') continue;
- const selector = Array.isArray(b?.selector) ? b.selector : [];
- if (selector.length === 0) continue;
- if (selector.every((s) => removedOutbounds.has(s))) emptied.push(tag);
- }
- return emptied;
- }
- /**
- * Single source of truth for how a deletion affects one rule, shared by the
- * preview (`ruleImpacts`) and the mutation (`applyCleanup`) so the two can never
- * disagree. Returns null when the rule is untouched; otherwise `keeps` names the
- * surviving destination, or is '' when none remains and the rule must be dropped.
- */
- function classifyRule(
- rule: RuleObject,
- removedOutbounds: Set<string>,
- removedBalancers: Set<string>,
- ): { losesOut: boolean; losesBal: boolean; keeps: string } | null {
- const out = typeof rule?.outboundTag === 'string' ? rule.outboundTag : '';
- const bal = typeof rule?.balancerTag === 'string' ? rule.balancerTag : '';
- const losesOut = out !== '' && removedOutbounds.has(out);
- const losesBal = bal !== '' && removedBalancers.has(bal);
- if (!losesOut && !losesBal) return null;
- const keptOut = out !== '' && !losesOut ? out : '';
- const keptBal = bal !== '' && !losesBal ? bal : '';
- return { losesOut, losesBal, keeps: keptOut || keptBal };
- }
- function ruleImpacts(
- tt: XraySettingsValue,
- removedOutbounds: Set<string>,
- removedBalancers: Set<string>,
- ): RuleImpact[] {
- const impacts: RuleImpact[] = [];
- ruleList(tt).forEach((rule, index) => {
- const verdict = classifyRule(rule, removedOutbounds, removedBalancers);
- if (!verdict) return;
- impacts.push(
- verdict.keeps
- ? { index, label: ruleLabel(rule, index), fate: 'modified', keeps: verdict.keeps }
- : { index, label: ruleLabel(rule, index), fate: 'removed' },
- );
- });
- return impacts;
- }
- function applyCleanup(
- tt: XraySettingsValue,
- removedOutbounds: Set<string>,
- removedBalancers: Set<string>,
- ): void {
- if (tt.routing && Array.isArray(tt.routing.rules)) {
- const next: RuleObject[] = [];
- for (const rule of tt.routing.rules) {
- const verdict = classifyRule(rule, removedOutbounds, removedBalancers);
- if (!verdict) {
- next.push(rule);
- continue;
- }
- if (verdict.losesOut) delete rule.outboundTag;
- if (verdict.losesBal) delete rule.balancerTag;
- if (verdict.keeps) next.push(rule);
- }
- tt.routing.rules = next;
- }
- if (tt.routing && Array.isArray(tt.routing.balancers)) {
- const survivors: BalancerObject[] = [];
- for (const balancer of tt.routing.balancers) {
- if (!balancer) continue;
- if (removedBalancers.has(balancer.tag)) continue;
- if (removedOutbounds.size > 0 && Array.isArray(balancer.selector)) {
- balancer.selector = balancer.selector.filter((s) => !removedOutbounds.has(s));
- }
- if (typeof balancer.fallbackTag === 'string' && removedOutbounds.has(balancer.fallbackTag)) {
- balancer.fallbackTag = '';
- }
- survivors.push(balancer);
- }
- tt.routing.balancers = survivors;
- }
- if (removedOutbounds.size > 0 && Array.isArray(tt.outbounds)) {
- tt.outbounds = tt.outbounds.filter(
- (o) => !(typeof o?.tag === 'string' && removedOutbounds.has(o.tag)),
- );
- for (const outbound of tt.outbounds) {
- const sockopt = (outbound as { streamSettings?: { sockopt?: { dialerProxy?: string } } })
- ?.streamSettings?.sockopt;
- if (sockopt && typeof sockopt.dialerProxy === 'string' && removedOutbounds.has(sockopt.dialerProxy)) {
- delete sockopt.dialerProxy;
- }
- }
- }
- syncObservatories(tt);
- }
- function observersRemovedBy(
- tt: XraySettingsValue,
- removedOutbounds: Set<string>,
- removedBalancers: Set<string>,
- ): { observatory: boolean; burst: boolean } {
- const hadObservatory = !!tt.observatory;
- const hadBurst = !!tt.burstObservatory;
- if (!hadObservatory && !hadBurst) return { observatory: false, burst: false };
- const clone = JSON.parse(JSON.stringify(tt)) as XraySettingsValue;
- applyCleanup(clone, removedOutbounds, removedBalancers);
- return {
- observatory: hadObservatory && !clone.observatory,
- burst: hadBurst && !clone.burstObservatory,
- };
- }
- export function planBalancerDeletion(tt: XraySettingsValue, index: number): DeletionImpact {
- const tag = balancerTagAt(tt, index);
- if (!tag) return emptyImpact();
- const removedOutbounds = new Set<string>();
- const removedBalancers = new Set([tag]);
- const obs = observersRemovedBy(tt, removedOutbounds, removedBalancers);
- return {
- rules: ruleImpacts(tt, removedOutbounds, removedBalancers),
- balancers: [],
- observatory: obs.observatory,
- burst: obs.burst,
- };
- }
- export function applyBalancerDeletion(tt: XraySettingsValue, index: number): void {
- const tag = balancerTagAt(tt, index);
- if (!tag) {
- if (tt.routing && Array.isArray(tt.routing.balancers)) tt.routing.balancers.splice(index, 1);
- syncObservatories(tt);
- return;
- }
- applyCleanup(tt, new Set<string>(), new Set([tag]));
- }
- export function planOutboundDeletion(tt: XraySettingsValue, index: number): DeletionImpact {
- const tag = outboundTagAt(tt, index);
- if (!tag) return emptyImpact();
- const removedOutbounds = new Set([tag]);
- const cascaded = balancersEmptiedBy(tt, removedOutbounds);
- const removedBalancers = new Set(cascaded);
- const obs = observersRemovedBy(tt, removedOutbounds, removedBalancers);
- return {
- rules: ruleImpacts(tt, removedOutbounds, removedBalancers),
- balancers: cascaded.map((bTag) => ({ tag: bTag, reason: 'selectorEmptied' as const })),
- observatory: obs.observatory,
- burst: obs.burst,
- };
- }
- export function applyOutboundDeletion(tt: XraySettingsValue, index: number): void {
- const tag = outboundTagAt(tt, index);
- if (!tag) {
- if (Array.isArray(tt.outbounds)) tt.outbounds.splice(index, 1);
- syncObservatories(tt);
- return;
- }
- const removedOutbounds = new Set([tag]);
- const removedBalancers = new Set(balancersEmptiedBy(tt, removedOutbounds));
- applyCleanup(tt, removedOutbounds, removedBalancers);
- }
|