Skip to content

Commit b4124ee

Browse files
feat(frontend): detect routing rules that match every connection
A rule with no domain, ip or protocol whose port range spans every port matches all traffic, so sing-box stops there: the rules below it and the resolve step the generator appends for IPIfNonMatch never run.
1 parent ec927f4 commit b4124ee

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { describe, expect, it } from "vitest";
2+
import type { RoutingRule } from "../../../generated/bindings";
3+
import { isCatchAllRule } from "../helpers";
4+
5+
const rule = (patch: Partial<RoutingRule>): RoutingRule => ({
6+
id: "r1",
7+
remarks: "",
8+
enabled: true,
9+
outboundTag: "proxy",
10+
...patch,
11+
});
12+
13+
describe("isCatchAllRule", () => {
14+
it("flags a full port range with no other match field", () => {
15+
expect(isCatchAllRule(rule({ port: "0-65535" }))).toBe(true);
16+
expect(isCatchAllRule(rule({ port: "1-65535" }))).toBe(true);
17+
});
18+
19+
it("flags a full range assembled from several parts", () => {
20+
expect(isCatchAllRule(rule({ port: "1-1000,1001-65535" }))).toBe(true);
21+
});
22+
23+
it("ignores a range with a gap or a short tail", () => {
24+
expect(isCatchAllRule(rule({ port: "1-1000,1002-65535" }))).toBe(false);
25+
expect(isCatchAllRule(rule({ port: "0-65534" }))).toBe(false);
26+
expect(isCatchAllRule(rule({ port: "443" }))).toBe(false);
27+
});
28+
29+
it("ignores rules that also match on domain, ip or protocol", () => {
30+
expect(isCatchAllRule(rule({ port: "0-65535", domain: ["geosite:private"] }))).toBe(false);
31+
expect(isCatchAllRule(rule({ port: "0-65535", ip: ["geoip:ru"] }))).toBe(false);
32+
expect(isCatchAllRule(rule({ port: "0-65535", protocol: ["bittorrent"] }))).toBe(false);
33+
});
34+
35+
it("treats a single-network rule as narrower than catch-all", () => {
36+
expect(isCatchAllRule(rule({ port: "0-65535", network: "udp" }))).toBe(false);
37+
expect(isCatchAllRule(rule({ port: "0-65535", network: "tcp,udp" }))).toBe(true);
38+
});
39+
40+
it("ignores disabled rules and rules the backend drops for having no match field", () => {
41+
expect(isCatchAllRule(rule({ port: "0-65535", enabled: false }))).toBe(false);
42+
expect(isCatchAllRule(rule({}))).toBe(false);
43+
expect(isCatchAllRule(rule({ port: " " }))).toBe(false);
44+
});
45+
46+
it("ignores a malformed port list", () => {
47+
expect(isCatchAllRule(rule({ port: "0-abc" }))).toBe(false);
48+
});
49+
});

frontend/src/features/settings/helpers.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,37 @@ export function ruleSummary(
6363
return parts.join(" · ");
6464
}
6565

66+
const MAX_PORT = 65535;
67+
68+
function portCoversEveryPort(port: string | null | undefined): boolean {
69+
if (!port?.trim()) return false;
70+
const ranges: [number, number][] = [];
71+
for (const item of port.split(",")) {
72+
const part = item.trim();
73+
if (!part) continue;
74+
const [lo, hi] = part.includes("-") ? part.split("-") : [part, part];
75+
const from = Number(lo);
76+
const to = Number(hi);
77+
if (!Number.isInteger(from) || !Number.isInteger(to)) return false;
78+
ranges.push([Math.min(from, to), Math.max(from, to)]);
79+
}
80+
ranges.sort((a, b) => a[0] - b[0]);
81+
let reached = 0;
82+
for (const [from, to] of ranges) {
83+
if (from > reached + 1) break;
84+
reached = Math.max(reached, to);
85+
}
86+
return reached >= MAX_PORT;
87+
}
88+
89+
/** Whether the rule matches every connection, making the rules below it dead. */
90+
export function isCatchAllRule(rule: RoutingRule): boolean {
91+
if (!rule.enabled) return false;
92+
if (rule.domain?.length || rule.ip?.length || rule.protocol?.length) return false;
93+
if (rule.network && rule.network !== "tcp,udp") return false;
94+
return portCoversEveryPort(rule.port);
95+
}
96+
6697
export function ruleIcon(rule: RoutingRule): string {
6798
if (rule.outboundTag === "direct") return "near_me";
6899
if (rule.outboundTag === "block") return "block";

0 commit comments

Comments
 (0)