Skip to content

Commit a11a470

Browse files
committed
update core
1 parent 72a6a91 commit a11a470

File tree

92 files changed

+5181
-855
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

92 files changed

+5181
-855
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ test.*
1111
tsconfig2.json
1212

1313
**/*.Identifier
14+
/temp

src/localServer/BandwidthCount.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import {Transform, TransformCallback} from 'node:stream'
2+
import { logger } from './logger'
3+
4+
// 统计防抖常量:最小测量时长 & 上报限幅
5+
const MIN_SAMPLE_MS = 50; // 小于该时长的样本按 50ms 计
6+
const MAX_REPORT_MBPS = 1000; // 上报限幅,防极端爆表
7+
8+
export class BandwidthCount extends Transform {
9+
private count = 0
10+
private startTime = 0
11+
private endTime = 0
12+
private printed = false
13+
14+
constructor(private tab: string){
15+
super({
16+
readableHighWaterMark: 64 * 1024,
17+
writableHighWaterMark: 64 * 1024
18+
})
19+
}
20+
21+
public _transform(chunk: Buffer, encoding: BufferEncoding, callback: TransformCallback): void {
22+
if (!this.startTime) {
23+
this.startTime = Date.now()
24+
}
25+
this.count += chunk.length
26+
// logger(`${this.tab} start at ${this.startTime} BandwidthCount ${this.count} bytes`)
27+
this.push(chunk)
28+
callback()
29+
}
30+
31+
public _final(callback: (error?: Error | null | undefined) => void): void {
32+
this.endTime = Date.now()
33+
this.finishIfNeeded('normal')
34+
callback()
35+
}
36+
37+
public _destroy(error: Error | null, callback: (error?: Error | null) => void): void {
38+
this.endTime = Date.now()
39+
// error 可能为 null(例如主动 destroy()),也可能包含错误信息
40+
const reason = error ? `error: ${error.message}` : 'destroyed'
41+
this.finishIfNeeded('abnormal', reason)
42+
callback(error || undefined)
43+
}
44+
45+
public getTotalBytes() {
46+
return this.count
47+
}
48+
49+
private finishIfNeeded(kind: 'normal' | 'abnormal', reason?: string) {
50+
if (this.printed) return
51+
this.printed = true
52+
53+
if (!this.startTime) this.startTime = this.endTime || Date.now()
54+
55+
const endTs = this.endTime || Date.now()
56+
const durationMs = Math.max(1, endTs - this.startTime)
57+
const durationSec = durationMs / 1000
58+
59+
const avgBytesPerSec = this.count / durationSec
60+
const avgBitsPerSec = avgBytesPerSec * 8
61+
62+
const totalHuman = BandwidthCount.formatBytes(this.count)
63+
const avgHumanBytes = BandwidthCount.formatBytes(avgBytesPerSec)
64+
const avgMbps = (avgBitsPerSec / 1e6).toFixed(3)
65+
66+
const head = `${this.tab} ${kind === 'normal' ? 'end' : 'end(abnormal)'} at ${endTs}` +
67+
(reason ? ` reason=${reason}` : '')
68+
69+
if (!this.count) {
70+
logger(`${head} BandwidthCount ${this.count} bytes (no data)`)
71+
return
72+
}
73+
74+
logger(
75+
`${head} BandwidthCount ${this.count} bytes (${totalHuman}), ` +
76+
`duration ${durationSec.toFixed(3)}s, ` +
77+
`avg ${avgHumanBytes}/s (${avgMbps} Mbps)`
78+
)
79+
}
80+
81+
private static formatBytes(n: number): string {
82+
const units = ['B', 'KB', 'MB', 'GB', 'TB']
83+
let v = n
84+
let i = 0
85+
while (v >= 1024 && i < units.length - 1) {
86+
v /= 1024
87+
i++
88+
}
89+
return i === 0 ? `${Math.round(v)} ${units[i]}` : `${v.toFixed(2)} ${units[i]}`
90+
}
91+
}
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
// createRuleMatcher.ts
2+
3+
/* 固定 bypass 域名(根域与任意层级子域名都直连) */
4+
export const BYPASS_DOMAINS = [
5+
"localhost",
6+
".localhost",
7+
".local",
8+
".localdomain",
9+
".home.arpa",
10+
"host.docker.internal",
11+
"gateway.docker.internal",
12+
] as const;
13+
14+
/* 固定 bypass IPv4 段(直连) */
15+
export const BYPASS_IP_CIDRS = [
16+
"127.0.0.0/8",
17+
"10.0.0.0/8",
18+
"172.16.0.0/12",
19+
"192.168.0.0/16",
20+
"169.254.0.0/16",
21+
"100.64.0.0/10",
22+
"224.0.0.0/4",
23+
"0.0.0.0/8",
24+
"255.255.255.255/32",
25+
] as const;
26+
27+
export type FilterRule = {
28+
DOMAIN: string[];
29+
IP: string[];
30+
};
31+
32+
type CidrTuple = { base: number; maskBits: number };
33+
34+
/** 统一为“以 . 开头的后缀”并去重:["example.com",".foo"] -> [".example.com",".foo"] */
35+
function normalizeSuffixesUnique(list: string[] | undefined): string[] {
36+
const arr = (list ?? [])
37+
.map((s) => s.trim().toLowerCase())
38+
.filter(Boolean)
39+
.map((s) => (s.startsWith(".") ? s : "." + s));
40+
return Array.from(new Set(arr));
41+
}
42+
43+
/** "a.b.c.d" -> uint32(0..2^32-1);非法返回 null */
44+
function ipv4ToInt(ipStr: string): number | null {
45+
const m = ipStr.match(
46+
/^(25[0-5]|2[0-4]\d|1?\d?\d)\.(25[0-5]|2[0-4]\d|1?\d?\d)\.(25[0-5]|2[0-4]\d|1?\d?\d)\.(25[0-5]|2[0-4]\d|1?\d?\d)$/
47+
);
48+
if (!m) return null;
49+
const a = Number(m[1]),
50+
b = Number(m[2]),
51+
c = Number(m[3]),
52+
d = Number(m[4]);
53+
return (((a << 24) >>> 0) + (b << 16) + (c << 8) + d) >>> 0;
54+
}
55+
56+
function intToIpv4(n: number): string {
57+
return [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff].join(
58+
"."
59+
);
60+
}
61+
62+
/** 掩码位数 -> 掩码位图(uint32) */
63+
function maskToBits(maskBits: number): number {
64+
return maskBits === 0 ? 0 : (~((1 << (32 - maskBits)) - 1)) >>> 0;
65+
}
66+
67+
/** 掩码位数 -> 点分掩码 */
68+
function maskBitsToDottedMask(bits: number): string {
69+
if (bits <= 0) return "0.0.0.0";
70+
if (bits >= 32) return "255.255.255.255";
71+
const mask = (0xffffffff << (32 - bits)) >>> 0;
72+
return [
73+
(mask >>> 24) & 0xff,
74+
(mask >>> 16) & 0xff,
75+
(mask >>> 8) & 0xff,
76+
mask & 0xff,
77+
].join(".");
78+
}
79+
80+
/** 解析 "x.y.z.w/n" 或 "x.y.z.w" 为 { base, maskBits }(base 已按掩码归一化);非法返回 null */
81+
function toCidrTuple(s: string): CidrTuple | null {
82+
const parts = s.split("/");
83+
if (parts.length === 1) {
84+
const ip = ipv4ToInt(parts[0].trim());
85+
if (ip === null) return null;
86+
return { base: ip, maskBits: 32 };
87+
}
88+
if (parts.length === 2) {
89+
const ip = ipv4ToInt(parts[0].trim());
90+
const mask = Number(parts[1]);
91+
if (ip === null || !Number.isInteger(mask) || mask < 0 || mask > 32) return null;
92+
const base = (ip & maskToBits(mask)) >>> 0; // 归一化网络地址
93+
return { base, maskBits: mask };
94+
}
95+
return null;
96+
}
97+
98+
/** ip 是否落在 base/mask 中 */
99+
function inCidr(ip: number, base: number, maskBits: number): boolean {
100+
const mask = maskToBits(maskBits);
101+
return (ip & mask) === (base & mask);
102+
}
103+
104+
/** host 是否匹配 ".example.com"(含根域名相等) */
105+
function domainMatches(hostLower: string, suffixDotLower: string): boolean {
106+
const plain = suffixDotLower.slice(1);
107+
return hostLower === plain || hostLower.endsWith(suffixDotLower);
108+
}
109+
110+
export function createRuleMatcher(rules: FilterRule) {
111+
/* 原始规则副本(便于导出/调试) */
112+
const rawRules: FilterRule = {
113+
DOMAIN: [...(rules?.DOMAIN ?? [])],
114+
IP: [...(rules?.IP ?? [])],
115+
};
116+
117+
/* 编译用户域名/网段 */
118+
const domainSuffixes = normalizeSuffixesUnique(rawRules.DOMAIN);
119+
120+
const ipCidrs: CidrTuple[] = (rawRules.IP ?? [])
121+
.map((s) => s.trim())
122+
.filter(Boolean)
123+
.map(toCidrTuple)
124+
.filter((x): x is CidrTuple => !!x);
125+
126+
/* 编译固定 bypass */
127+
const bypassDomainSuffixes = normalizeSuffixesUnique([...BYPASS_DOMAINS]);
128+
const bypassIpCidrs: CidrTuple[] = [...BYPASS_IP_CIDRS]
129+
.map(toCidrTuple)
130+
.filter((x): x is CidrTuple => !!x);
131+
132+
function match(host: string): boolean {
133+
const h = host.trim();
134+
const ip = ipv4ToInt(h);
135+
136+
// 0) 固定 bypass 优先:命中则“排除在代理之外”
137+
if (ip !== null) {
138+
for (const { base, maskBits } of bypassIpCidrs) {
139+
if (inCidr(ip, base, maskBits)) return false;
140+
}
141+
} else {
142+
const lh = h.toLowerCase();
143+
for (const suf of bypassDomainSuffixes) {
144+
if (domainMatches(lh, suf)) return false;
145+
}
146+
}
147+
148+
// 1) 用户 IP 规则
149+
if (ip !== null) {
150+
for (const { base, maskBits } of ipCidrs) {
151+
if (inCidr(ip, base, maskBits)) return true;
152+
}
153+
return false;
154+
}
155+
156+
// 2) 用户 DOMAIN 规则
157+
const lh = h.toLowerCase();
158+
for (const suf of domainSuffixes) {
159+
if (domainMatches(lh, suf)) return true;
160+
}
161+
return false;
162+
}
163+
164+
/**
165+
* 生成 PAC 文本(bypass → DIRECT, 用户规则 → proxy, 默认 DIRECT)
166+
* @param proxy 例:"PROXY 127.0.0.1:1080; DIRECT"
167+
* @param opts.embedMeta 是否在 PAC 顶部注释中嵌入 raw/compiled 元数据
168+
*/
169+
function pac(
170+
proxy = "SOCKS5 127.0.0.1:3002; SOCKS 127.0.0.1:3002; PROXY 127.0.0.1:3002; DIRECT",
171+
opts?: { embedMeta?: boolean }
172+
): string {
173+
174+
175+
// 直连(bypass)域名条件
176+
const bypassDomainChecks = bypassDomainSuffixes.map((suf) => {
177+
const plain = suf.slice(1);
178+
return `(host === "${plain}" || dnsDomainIs(host, "${suf}"))`;
179+
});
180+
181+
// 直连(bypass)IP 条件(网络地址 + 掩码)
182+
const bypassIpChecks = bypassIpCidrs.map((c) => {
183+
const net = intToIpv4(c.base & maskToBits(c.maskBits));
184+
const mask = maskBitsToDottedMask(c.maskBits);
185+
return `isInNet(host, "${net}", "${mask}")`;
186+
});
187+
188+
// 用户域名条件
189+
const userDomainChecks = domainSuffixes.map((suf) => {
190+
const plain = suf.slice(1);
191+
return `(host === "${plain}" || dnsDomainIs(host, "${suf}"))`;
192+
});
193+
194+
// 用户 IP 条件(去掉与 bypass 完全相同的条目以免重复)
195+
const bypassSet = new Set(
196+
bypassIpCidrs.map(
197+
(c) => `${intToIpv4(c.base & maskToBits(c.maskBits))}/${c.maskBits}`
198+
)
199+
);
200+
const userIpChecks = ipCidrs
201+
.filter(
202+
(c) =>
203+
!bypassSet.has(
204+
`${intToIpv4(c.base & maskToBits(c.maskBits))}/${c.maskBits}`
205+
)
206+
)
207+
.map((c) => {
208+
const net = intToIpv4(c.base & maskToBits(c.maskBits));
209+
const mask = maskBitsToDottedMask(c.maskBits);
210+
return `isInNet(host, "${net}", "${mask}")`;
211+
});
212+
213+
const meta =
214+
opts?.embedMeta
215+
? `/* createRuleMatcher meta
216+
generatedAt: ${new Date().toISOString()}
217+
rawRules: ${JSON.stringify(rawRules)}
218+
compiled: ${JSON.stringify({
219+
domainSuffixes,
220+
ipCidrs: ipCidrs.map((c) => ({
221+
network: intToIpv4(c.base & maskToBits(c.maskBits)),
222+
maskBits: c.maskBits,
223+
})),
224+
bypassDomainSuffixes,
225+
bypassIpCidrs: BYPASS_IP_CIDRS,
226+
})}
227+
*/\n`
228+
: "";
229+
230+
const pac = `${meta}function FindProxyForURL(url, host) {
231+
if (!host) return "DIRECT";
232+
// 仅聚焦 IPv4;IPv6 字面量直接直连
233+
if (host.indexOf(":") !== -1) return "DIRECT";
234+
// 标准化域名大小写
235+
host = host.toLowerCase();
236+
237+
// 固定 bypass:先直连
238+
${bypassDomainChecks.length ? `if (${bypassDomainChecks.join(" ||\n ")}) return "DIRECT";` : ""}
239+
${bypassIpChecks.length ? `if (${bypassIpChecks.join(" ||\n ")}) return "DIRECT";` : ""}
240+
241+
// 用户规则:命中则直连;其余全部走代理
242+
${userDomainChecks.length ? `if (${userDomainChecks.join(" ||\n ")}) return "DIRECT";` : ""}
243+
${userIpChecks.length ? `if (${userIpChecks.join(" ||\n ")}) return "DIRECT";` : ""}
244+
245+
return "${proxy}";
246+
}
247+
`;
248+
return pac;
249+
}
250+
251+
/** 导出原始规则(浅拷贝) */
252+
function exportRules(): FilterRule {
253+
return { DOMAIN: [...rawRules.DOMAIN], IP: [...rawRules.IP] };
254+
}
255+
256+
/** 导出编译视图,便于核对或日志打印 */
257+
function compiled() {
258+
return {
259+
DOMAIN_SUFFIXES: [...domainSuffixes],
260+
IP_CIDRS: ipCidrs.map((c) => ({
261+
network: intToIpv4(c.base & maskToBits(c.maskBits)),
262+
maskBits: c.maskBits,
263+
})),
264+
BYPASS_DOMAINS: [...bypassDomainSuffixes],
265+
BYPASS_IP_CIDRS: [...BYPASS_IP_CIDRS],
266+
};
267+
}
268+
269+
/** 便捷调试 dump(美化 JSON) */
270+
function dump(): string {
271+
return JSON.stringify({ rules: exportRules(), compiled: compiled() }, null, 2);
272+
}
273+
274+
return { match, pac, rules, compiled, dump };
275+
}
276+
277+
/* -------------------------------------------
278+
使用示例:
279+
280+
const matcher = createRuleMatcher({
281+
DOMAIN: ["example.com", ".internal.corp"],
282+
IP: ["8.8.8.8", "203.0.113.0/24"],
283+
});
284+
285+
// 运行时匹配
286+
matcher.match("api.example.com"); // true
287+
matcher.match("localhost"); // false(bypass)
288+
matcher.match("192.168.1.10"); // false(bypass)
289+
matcher.match("8.8.8.8"); // true
290+
291+
// 生成 PAC(可供 /pac 路由返回)
292+
const pacText = matcher.pac("PROXY 127.0.0.1:1080; DIRECT", { embedMeta: true });
293+
294+
------------------------------------------- */

0 commit comments

Comments
 (0)