@singbox-iac/cli 0.1.17 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-en.md +117 -20
- package/README.md +54 -2
- package/dist/cli/commands/apply.js +3 -0
- package/dist/cli/commands/apply.js.map +1 -1
- package/dist/cli/commands/build.js +15 -1
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/diagnose.js +21 -5
- package/dist/cli/commands/diagnose.js.map +1 -1
- package/dist/cli/commands/proxifier.js +13 -4
- package/dist/cli/commands/proxifier.js.map +1 -1
- package/dist/cli/commands/reload.js +14 -1
- package/dist/cli/commands/reload.js.map +1 -1
- package/dist/cli/commands/restart.js +52 -8
- package/dist/cli/commands/restart.js.map +1 -1
- package/dist/cli/commands/rollback.js +1 -0
- package/dist/cli/commands/rollback.js.map +1 -1
- package/dist/cli/commands/setup.js +2 -1
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/start.js +92 -11
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/status.js +12 -6
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/stop.js +24 -8
- package/dist/cli/commands/stop.js.map +1 -1
- package/dist/cli/commands/verify.js +20 -2
- package/dist/cli/commands/verify.js.map +1 -1
- package/dist/config/load-config.js +24 -1
- package/dist/config/load-config.js.map +1 -1
- package/dist/config/schema.d.ts +173 -29
- package/dist/config/schema.js +63 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/domain/dns-plan.d.ts +11 -1
- package/dist/domain/intent.d.ts +2 -2
- package/dist/domain/verification-plan.d.ts +3 -3
- package/dist/modules/authoring/index.js +22 -2
- package/dist/modules/authoring/index.js.map +1 -1
- package/dist/modules/bundle-registry/index.d.ts +8 -2
- package/dist/modules/bundle-registry/index.js +54 -13
- package/dist/modules/bundle-registry/index.js.map +1 -1
- package/dist/modules/compiler/index.js +79 -12
- package/dist/modules/compiler/index.js.map +1 -1
- package/dist/modules/desktop-runtime/index.d.ts +50 -0
- package/dist/modules/desktop-runtime/index.js +236 -16
- package/dist/modules/desktop-runtime/index.js.map +1 -1
- package/dist/modules/diagnostics/index.js +93 -0
- package/dist/modules/diagnostics/index.js.map +1 -1
- package/dist/modules/dns-plan/index.js +73 -12
- package/dist/modules/dns-plan/index.js.map +1 -1
- package/dist/modules/doctor/index.js +41 -0
- package/dist/modules/doctor/index.js.map +1 -1
- package/dist/modules/intent/index.js +2 -2
- package/dist/modules/intent/index.js.map +1 -1
- package/dist/modules/layered-authoring/index.js +1 -1
- package/dist/modules/layered-authoring/index.js.map +1 -1
- package/dist/modules/manager/index.d.ts +3 -1
- package/dist/modules/manager/index.js +37 -2
- package/dist/modules/manager/index.js.map +1 -1
- package/dist/modules/natural-language/index.d.ts +3 -2
- package/dist/modules/natural-language/index.js +83 -35
- package/dist/modules/natural-language/index.js.map +1 -1
- package/dist/modules/proxifier/index.js +3 -4
- package/dist/modules/proxifier/index.js.map +1 -1
- package/dist/modules/runtime-mode/index.d.ts +1 -1
- package/dist/modules/runtime-mode/index.js +0 -5
- package/dist/modules/runtime-mode/index.js.map +1 -1
- package/dist/modules/runtime-watchdog/index.d.ts +10 -0
- package/dist/modules/runtime-watchdog/index.js +33 -0
- package/dist/modules/runtime-watchdog/index.js.map +1 -1
- package/dist/modules/status/index.d.ts +13 -0
- package/dist/modules/status/index.js +106 -9
- package/dist/modules/status/index.js.map +1 -1
- package/dist/modules/update/index.js +3 -1
- package/dist/modules/update/index.js.map +1 -1
- package/dist/modules/verification/index.d.ts +27 -5
- package/dist/modules/verification/index.js +542 -20
- package/dist/modules/verification/index.js.map +1 -1
- package/dist/modules/verification-plan/index.js +16 -6
- package/dist/modules/verification-plan/index.js.map +1 -1
- package/docs/natural-language-authoring.md +6 -0
- package/docs/runtime-modes.md +23 -7
- package/docs/runtime-on-macos.md +54 -9
- package/docs/sing-box-config-primer.md +10 -1
- package/package.json +1 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { access } from "node:fs/promises";
|
|
4
|
-
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { chmod, mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
5
5
|
import net from "node:net";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
8
|
+
import { isProcessRoot } from "../desktop-runtime/index.js";
|
|
8
9
|
import { checkConfig, resolveSingBoxBinary } from "../manager/index.js";
|
|
9
10
|
import { resolveChromeDependency } from "../runtime-dependencies/index.js";
|
|
10
11
|
export function assertVerificationReportPassed(report) {
|
|
@@ -16,6 +17,7 @@ export function assertVerificationReportPassed(report) {
|
|
|
16
17
|
.map((scenario) => `- ${scenario.name}\n url: ${scenario.url}\n details: ${scenario.details}`)
|
|
17
18
|
.join("\n")}`);
|
|
18
19
|
}
|
|
20
|
+
const nativeProcessProbeUrl = "https://example.com/";
|
|
19
21
|
export async function verifyConfigRoutes(input) {
|
|
20
22
|
const singBoxBinary = await resolveSingBoxBinary(input.singBoxBinary);
|
|
21
23
|
const requestBinary = await resolveCurlBinary();
|
|
@@ -90,20 +92,175 @@ export function verifyDnsPlan(plan, dnsPlan) {
|
|
|
90
92
|
export function verifyAppPlan(plan, config) {
|
|
91
93
|
const route = asObject(config.route, "Config is missing route.");
|
|
92
94
|
const rules = ensureArray(route.rules, "Route is missing rules.");
|
|
93
|
-
const proxifierProtected = rules.some((rule) => Array.isArray(rule.inbound) &&
|
|
94
|
-
rule.inbound.includes("in-proxifier") &&
|
|
95
|
-
rule.action === "route" &&
|
|
96
|
-
rule.outbound === "Process-Proxy");
|
|
97
95
|
return plan.appChecks.map((check) => ({
|
|
98
96
|
app: check.app,
|
|
99
|
-
passed: check
|
|
100
|
-
|
|
97
|
+
passed: hasExpectedProcessAwareRoute(rules, check),
|
|
98
|
+
expectedCaptureBackend: check.expectedCaptureBackend,
|
|
101
99
|
expectedOutboundGroup: check.expectedOutboundGroup,
|
|
102
|
-
details: check
|
|
103
|
-
? "Protected in-proxifier route is present."
|
|
104
|
-
: "No special app inbound is required.",
|
|
100
|
+
details: describeProcessAwareRouteResult(rules, check),
|
|
105
101
|
}));
|
|
106
102
|
}
|
|
103
|
+
export async function verifyNativeProcessE2E(input) {
|
|
104
|
+
const nativeChecks = input.plan.appChecks.filter((check) => check.expectedCaptureBackend === "native-process");
|
|
105
|
+
if (nativeChecks.length === 0) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
if (!isProcessRoot()) {
|
|
109
|
+
return nativeChecks.map((check) => ({
|
|
110
|
+
app: check.app,
|
|
111
|
+
status: "SKIP",
|
|
112
|
+
expectedOutboundGroup: check.expectedOutboundGroup,
|
|
113
|
+
details: "Native-process e2e requires root. Re-run `sudo singbox-iac verify app -c <builder.config.yaml>`.",
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
const singBoxBinary = await resolveSingBoxBinary(input.singBoxBinary);
|
|
117
|
+
const cCompilerBinary = await resolveCCompilerBinary();
|
|
118
|
+
const rawConfig = JSON.parse(await readFile(input.configPath, "utf8"));
|
|
119
|
+
const results = [];
|
|
120
|
+
for (const check of nativeChecks) {
|
|
121
|
+
const prepared = await prepareVerificationConfig(rawConfig);
|
|
122
|
+
const conflictingOutbound = chooseConflictingOutbound(prepared.config, check.expectedOutboundGroup);
|
|
123
|
+
if (!conflictingOutbound) {
|
|
124
|
+
results.push({
|
|
125
|
+
app: check.app,
|
|
126
|
+
status: "SKIP",
|
|
127
|
+
expectedOutboundGroup: check.expectedOutboundGroup,
|
|
128
|
+
details: "No conflicting fallback outbound is available in the compiled config for native-process e2e.",
|
|
129
|
+
});
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
injectNativeProcessProbeRule(prepared.config, conflictingOutbound);
|
|
133
|
+
const processName = selectNativeProcessProbeName(prepared.config, check.expectedOutboundGroup);
|
|
134
|
+
if (!processName) {
|
|
135
|
+
results.push({
|
|
136
|
+
app: check.app,
|
|
137
|
+
status: "SKIP",
|
|
138
|
+
expectedOutboundGroup: check.expectedOutboundGroup,
|
|
139
|
+
details: "No concrete process_name matcher was available for native-process e2e. Add a plain processName matcher to the bundle or explicit process policy.",
|
|
140
|
+
});
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const runDir = await mkdtemp(path.join(tmpdir(), "singbox-iac-native-process-"));
|
|
144
|
+
const verifyConfigPath = path.join(runDir, "native-process.config.json");
|
|
145
|
+
const logPath = path.join(runDir, "native-process.log");
|
|
146
|
+
const probeBinaryPath = path.join(runDir, processName);
|
|
147
|
+
await compileNativeProcessProbe({
|
|
148
|
+
runDir,
|
|
149
|
+
compilerBinary: cCompilerBinary,
|
|
150
|
+
probeBinaryPath,
|
|
151
|
+
});
|
|
152
|
+
injectNativeProcessPathProbeRule(prepared.config, probeBinaryPath, check.expectedOutboundGroup);
|
|
153
|
+
await writeFile(verifyConfigPath, `${JSON.stringify(prepared.config, null, 2)}\n`, "utf8");
|
|
154
|
+
await checkConfig({ configPath: verifyConfigPath, singBoxBinary });
|
|
155
|
+
const logBuffer = { text: "" };
|
|
156
|
+
const appender = async (chunk) => {
|
|
157
|
+
const text = chunk.toString();
|
|
158
|
+
logBuffer.text += text;
|
|
159
|
+
await writeFile(logPath, text, { encoding: "utf8", flag: "a" });
|
|
160
|
+
};
|
|
161
|
+
const child = spawn(singBoxBinary, ["run", "-c", verifyConfigPath], {
|
|
162
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
163
|
+
});
|
|
164
|
+
child.stdout.on("data", (chunk) => void appender(chunk));
|
|
165
|
+
child.stderr.on("data", (chunk) => void appender(chunk));
|
|
166
|
+
const exitPromise = new Promise((resolve, reject) => {
|
|
167
|
+
child.on("error", reject);
|
|
168
|
+
child.on("close", (code) => resolve(code ?? 0));
|
|
169
|
+
});
|
|
170
|
+
try {
|
|
171
|
+
await waitForLog(logBuffer, /sing-box started/, 15_000, "Timed out waiting for sing-box startup during native-process verification.");
|
|
172
|
+
const scenario = await verifyNativeProcessRuntimeScenario({
|
|
173
|
+
id: `native-process-${slugifyProcessName(processName)}`,
|
|
174
|
+
name: `native-process e2e for ${check.app}`,
|
|
175
|
+
url: nativeProcessProbeUrl,
|
|
176
|
+
inboundTag: "in-tun",
|
|
177
|
+
proxyPort: prepared.mixedPort,
|
|
178
|
+
expectedOutboundGroup: check.expectedOutboundGroup,
|
|
179
|
+
processName,
|
|
180
|
+
probeBinaryPath,
|
|
181
|
+
}, logBuffer);
|
|
182
|
+
results.push({
|
|
183
|
+
app: check.app,
|
|
184
|
+
status: scenario.passed ? "PASS" : "FAIL",
|
|
185
|
+
expectedOutboundGroup: check.expectedOutboundGroup,
|
|
186
|
+
processName,
|
|
187
|
+
details: scenario.passed
|
|
188
|
+
? `Probe binary=${probeBinaryPath}; compiled process_name=${processName}; conflicting fallback=${conflictingOutbound}. ${scenario.details}`
|
|
189
|
+
: `Probe binary=${probeBinaryPath}; compiled process_name=${processName}; conflicting fallback=${conflictingOutbound}. ${scenario.details}`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
results.push({
|
|
194
|
+
app: check.app,
|
|
195
|
+
status: "FAIL",
|
|
196
|
+
expectedOutboundGroup: check.expectedOutboundGroup,
|
|
197
|
+
processName,
|
|
198
|
+
details: error instanceof Error ? error.message : String(error),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
child.kill("SIGINT");
|
|
203
|
+
await Promise.race([exitPromise, new Promise((resolve) => setTimeout(resolve, 2_000))]);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return results;
|
|
207
|
+
}
|
|
208
|
+
export function assertNativeProcessE2EPassed(results) {
|
|
209
|
+
const failures = results.filter((result) => result.status === "FAIL");
|
|
210
|
+
if (failures.length === 0) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
throw new Error(`Native-process e2e failed:\n${failures
|
|
214
|
+
.map((result) => `- ${result.app}${result.processName ? ` (${result.processName})` : ""}: ${result.details}`)
|
|
215
|
+
.join("\n")}`);
|
|
216
|
+
}
|
|
217
|
+
function hasExpectedProcessAwareRoute(rules, check) {
|
|
218
|
+
switch (check.expectedCaptureBackend) {
|
|
219
|
+
case "fallback":
|
|
220
|
+
return true;
|
|
221
|
+
case "proxifier":
|
|
222
|
+
return rules.some((rule) => Array.isArray(rule.inbound) &&
|
|
223
|
+
rule.inbound.includes("in-proxifier") &&
|
|
224
|
+
rule.outbound === check.expectedOutboundGroup);
|
|
225
|
+
case "native-process":
|
|
226
|
+
return rules.some((rule) => rule.outbound === check.expectedOutboundGroup &&
|
|
227
|
+
(Array.isArray(rule.process_name) ||
|
|
228
|
+
Array.isArray(rule.process_path) ||
|
|
229
|
+
Array.isArray(rule.process_path_regex)));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function describeProcessAwareRouteResult(rules, check) {
|
|
233
|
+
if (check.expectedCaptureBackend === "fallback") {
|
|
234
|
+
return "No capture backend was required for this app check.";
|
|
235
|
+
}
|
|
236
|
+
const matchedRule = rules.find((rule) => {
|
|
237
|
+
if (check.expectedCaptureBackend === "proxifier") {
|
|
238
|
+
return (Array.isArray(rule.inbound) &&
|
|
239
|
+
rule.inbound.includes("in-proxifier") &&
|
|
240
|
+
rule.outbound === check.expectedOutboundGroup);
|
|
241
|
+
}
|
|
242
|
+
return (rule.outbound === check.expectedOutboundGroup &&
|
|
243
|
+
(Array.isArray(rule.process_name) ||
|
|
244
|
+
Array.isArray(rule.process_path) ||
|
|
245
|
+
Array.isArray(rule.process_path_regex)));
|
|
246
|
+
});
|
|
247
|
+
if (!matchedRule) {
|
|
248
|
+
return `Expected ${check.expectedCaptureBackend} rule for ${check.expectedOutboundGroup}, but none was found.`;
|
|
249
|
+
}
|
|
250
|
+
const matchers = [
|
|
251
|
+
Array.isArray(matchedRule.inbound) ? `inbound=${matchedRule.inbound.join(",")}` : undefined,
|
|
252
|
+
Array.isArray(matchedRule.process_name)
|
|
253
|
+
? `process_name=${matchedRule.process_name.join(",")}`
|
|
254
|
+
: undefined,
|
|
255
|
+
Array.isArray(matchedRule.process_path)
|
|
256
|
+
? `process_path=${matchedRule.process_path.join(",")}`
|
|
257
|
+
: undefined,
|
|
258
|
+
Array.isArray(matchedRule.process_path_regex)
|
|
259
|
+
? `process_path_regex=${matchedRule.process_path_regex.join(",")}`
|
|
260
|
+
: undefined,
|
|
261
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
262
|
+
return `Matched ${check.expectedCaptureBackend} route to ${check.expectedOutboundGroup}${matchers.length > 0 ? ` (${matchers.join("; ")})` : ""}.`;
|
|
263
|
+
}
|
|
107
264
|
export function verifyProtocolPlan(plan, config) {
|
|
108
265
|
const route = asObject(config.route, "Config is missing route.");
|
|
109
266
|
const rules = ensureArray(route.rules, "Route is missing rules.");
|
|
@@ -277,6 +434,52 @@ async function verifyRuntimeScenario(scenario, logBuffer, requestBinary) {
|
|
|
277
434
|
expectedOutboundTag: scenario.expectedOutboundTag,
|
|
278
435
|
};
|
|
279
436
|
}
|
|
437
|
+
async function verifyNativeProcessRuntimeScenario(scenario, logBuffer) {
|
|
438
|
+
const hostname = new URL(scenario.url).hostname;
|
|
439
|
+
const inboundLog = new RegExp(`inbound/mixed\\[${escapeRegExp(scenario.inboundTag)}\\]: inbound connection to ${escapeRegExp(hostname)}:443`);
|
|
440
|
+
const routeLogAlternatives = [
|
|
441
|
+
new RegExp(`router: match\\[\\d+\\].*process_path[^\\n]*${escapeRegExp(scenario.probeBinaryPath)}[^\\n]*=> route\\(${escapeRegExp(scenario.expectedOutboundGroup)}\\)`),
|
|
442
|
+
new RegExp(`router: match\\[\\d+\\].*process_path[^\\n]*${escapeRegExp(path.basename(scenario.probeBinaryPath))}[^\\n]*=> route\\(${escapeRegExp(scenario.expectedOutboundGroup)}\\)`),
|
|
443
|
+
new RegExp(`router: match\\[\\d+\\].*process_name[^\\n]*${escapeRegExp(scenario.processName)}[^\\n]*=> route\\(${escapeRegExp(scenario.expectedOutboundGroup)}\\)`),
|
|
444
|
+
];
|
|
445
|
+
let lastFailure = `Expected native-process logs were not observed for ${hostname} within the timeout.`;
|
|
446
|
+
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
447
|
+
const offset = logBuffer.text.length;
|
|
448
|
+
const requestResult = await runNativeProcessProbeScenario({
|
|
449
|
+
probeBinaryPath: scenario.probeBinaryPath,
|
|
450
|
+
proxyPort: scenario.proxyPort,
|
|
451
|
+
targetHost: hostname,
|
|
452
|
+
targetPort: 443,
|
|
453
|
+
});
|
|
454
|
+
const excerpt = await waitForScenarioLogsWithAlternatives(logBuffer, offset, [inboundLog], routeLogAlternatives, 20_000);
|
|
455
|
+
const requestFailure = detectRequestFailure(requestResult);
|
|
456
|
+
if (excerpt !== undefined) {
|
|
457
|
+
return {
|
|
458
|
+
name: scenario.name,
|
|
459
|
+
passed: true,
|
|
460
|
+
details: requestFailure === undefined
|
|
461
|
+
? excerpt.trim()
|
|
462
|
+
: `${excerpt.trim()}\nRequest note: ${requestFailure}`,
|
|
463
|
+
url: scenario.url,
|
|
464
|
+
inboundTag: scenario.inboundTag,
|
|
465
|
+
expectedOutboundTag: scenario.expectedOutboundGroup,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
lastFailure =
|
|
469
|
+
requestFailure ?? `Expected native-process logs were not observed for ${hostname} within the timeout.`;
|
|
470
|
+
if (attempt < 2) {
|
|
471
|
+
await sleep(500);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
name: scenario.name,
|
|
476
|
+
passed: false,
|
|
477
|
+
details: lastFailure,
|
|
478
|
+
url: scenario.url,
|
|
479
|
+
inboundTag: scenario.inboundTag,
|
|
480
|
+
expectedOutboundTag: scenario.expectedOutboundGroup,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
280
483
|
export function isRouteLevelProxySuccess(scenario, requestResult) {
|
|
281
484
|
if (scenario.inboundTag !== "in-proxifier") {
|
|
282
485
|
return false;
|
|
@@ -344,18 +547,59 @@ export async function resolveCurlBinary(explicitPath) {
|
|
|
344
547
|
}
|
|
345
548
|
throw new Error("Unable to find a usable curl binary for route verification.");
|
|
346
549
|
}
|
|
550
|
+
export async function resolveCCompilerBinary(explicitPath) {
|
|
551
|
+
const candidates = [
|
|
552
|
+
explicitPath,
|
|
553
|
+
process.env.CC,
|
|
554
|
+
"/usr/bin/cc",
|
|
555
|
+
"/usr/bin/clang",
|
|
556
|
+
...resolvePathCandidates("cc"),
|
|
557
|
+
...resolvePathCandidates("clang"),
|
|
558
|
+
].filter((candidate) => typeof candidate === "string" && candidate.length > 0);
|
|
559
|
+
for (const candidate of candidates) {
|
|
560
|
+
if (await isExecutable(candidate)) {
|
|
561
|
+
return candidate;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
throw new Error("Unable to find a usable C compiler for native-process verification.");
|
|
565
|
+
}
|
|
347
566
|
export async function prepareVerificationConfig(config) {
|
|
348
567
|
const cloned = structuredClone(config);
|
|
349
568
|
const mixedPort = await findAvailablePort();
|
|
350
569
|
const proxifierPort = await findAvailablePort();
|
|
351
570
|
const inbounds = ensureArray(cloned.inbounds, "Config is missing inbounds.");
|
|
571
|
+
let hasMixedInbound = false;
|
|
572
|
+
let hasProxifierInbound = false;
|
|
352
573
|
for (const inbound of inbounds) {
|
|
353
574
|
if (inbound.tag === "in-mixed") {
|
|
575
|
+
hasMixedInbound = true;
|
|
354
576
|
inbound.listen_port = mixedPort;
|
|
355
577
|
}
|
|
356
578
|
if (inbound.tag === "in-proxifier") {
|
|
579
|
+
hasProxifierInbound = true;
|
|
357
580
|
inbound.listen_port = proxifierPort;
|
|
358
581
|
}
|
|
582
|
+
if (inbound.tag === "in-tun") {
|
|
583
|
+
for (const key of Object.keys(inbound)) {
|
|
584
|
+
delete inbound[key];
|
|
585
|
+
}
|
|
586
|
+
Object.assign(inbound, {
|
|
587
|
+
type: "mixed",
|
|
588
|
+
tag: "in-tun",
|
|
589
|
+
listen: "127.0.0.1",
|
|
590
|
+
listen_port: mixedPort,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (!hasMixedInbound &&
|
|
595
|
+
!hasProxifierInbound &&
|
|
596
|
+
!inbounds.some((inbound) => inbound.tag === "in-tun")) {
|
|
597
|
+
inbounds.push({
|
|
598
|
+
type: "mixed",
|
|
599
|
+
tag: "in-mixed",
|
|
600
|
+
listen: "127.0.0.1",
|
|
601
|
+
listen_port: mixedPort,
|
|
602
|
+
});
|
|
359
603
|
}
|
|
360
604
|
cloned.log = { level: "debug" };
|
|
361
605
|
const dns = asObject(cloned.dns, "Config is missing dns.");
|
|
@@ -365,9 +609,16 @@ export async function prepareVerificationConfig(config) {
|
|
|
365
609
|
tag: "dns-local-verify",
|
|
366
610
|
},
|
|
367
611
|
];
|
|
612
|
+
dns.rules = [];
|
|
368
613
|
dns.final = "dns-local-verify";
|
|
614
|
+
dns.reverse_mapping = false;
|
|
369
615
|
const route = asObject(cloned.route, "Config is missing route.");
|
|
370
616
|
route.default_domain_resolver = "dns-local-verify";
|
|
617
|
+
if (typeof cloned.experimental === "object" && cloned.experimental !== null) {
|
|
618
|
+
const experimental = asObject(cloned.experimental, "Config has an invalid experimental block.");
|
|
619
|
+
const { cache_file: _cacheFile, ...remaining } = experimental;
|
|
620
|
+
cloned.experimental = Object.keys(remaining).length === 0 ? undefined : remaining;
|
|
621
|
+
}
|
|
371
622
|
const outbounds = ensureArray(cloned.outbounds, "Config is missing outbounds.");
|
|
372
623
|
const globalIndex = outbounds.findIndex((outbound) => outbound.tag === "Global");
|
|
373
624
|
if (globalIndex >= 0) {
|
|
@@ -389,6 +640,59 @@ export async function prepareVerificationConfig(config) {
|
|
|
389
640
|
proxifierPort,
|
|
390
641
|
};
|
|
391
642
|
}
|
|
643
|
+
export function chooseConflictingOutbound(config, expectedOutboundGroup) {
|
|
644
|
+
const outbounds = ensureArray(config.outbounds, "Config is missing outbounds.");
|
|
645
|
+
const preferred = ["direct", "Global", "AI-Out", "Dev-Common-Out", "Stitch-Out", "Process-Proxy"];
|
|
646
|
+
for (const tag of preferred) {
|
|
647
|
+
if (tag === expectedOutboundGroup) {
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (outbounds.some((outbound) => outbound.tag === tag)) {
|
|
651
|
+
return tag;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return outbounds
|
|
655
|
+
.map((outbound) => (typeof outbound.tag === "string" ? outbound.tag : undefined))
|
|
656
|
+
.find((tag) => typeof tag === "string" && tag !== expectedOutboundGroup);
|
|
657
|
+
}
|
|
658
|
+
export function injectNativeProcessProbeRule(config, conflictingOutbound) {
|
|
659
|
+
const route = asObject(config.route, "Config is missing route.");
|
|
660
|
+
const rules = ensureArray(route.rules, "Route is missing rules.");
|
|
661
|
+
const probeHost = new URL(nativeProcessProbeUrl).hostname;
|
|
662
|
+
const insertionIndex = findLastProcessAwareRuleIndex(rules) + 1;
|
|
663
|
+
rules.splice(insertionIndex, 0, {
|
|
664
|
+
domain: [probeHost],
|
|
665
|
+
action: "route",
|
|
666
|
+
outbound: conflictingOutbound,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
export function injectNativeProcessPathProbeRule(config, probeBinaryPath, expectedOutboundGroup) {
|
|
670
|
+
const route = asObject(config.route, "Config is missing route.");
|
|
671
|
+
const rules = ensureArray(route.rules, "Route is missing rules.");
|
|
672
|
+
const insertionIndex = findLastProcessAwareRuleIndex(rules) + 1;
|
|
673
|
+
rules.splice(insertionIndex, 0, {
|
|
674
|
+
process_path: [probeBinaryPath],
|
|
675
|
+
action: "route",
|
|
676
|
+
outbound: expectedOutboundGroup,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
export function selectNativeProcessProbeName(config, expectedOutboundGroup) {
|
|
680
|
+
const route = asObject(config.route, "Config is missing route.");
|
|
681
|
+
const rules = ensureArray(route.rules, "Route is missing rules.");
|
|
682
|
+
for (const rule of rules) {
|
|
683
|
+
if (rule.outbound !== expectedOutboundGroup || !Array.isArray(rule.process_name)) {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
const candidate = rule.process_name.find((value) => typeof value === "string" &&
|
|
687
|
+
value.length > 0 &&
|
|
688
|
+
!value.includes("/") &&
|
|
689
|
+
!value.includes("*"));
|
|
690
|
+
if (candidate) {
|
|
691
|
+
return candidate;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
392
696
|
export function validateConfigInvariants(config) {
|
|
393
697
|
const route = asObject(config.route, "Config is missing route.");
|
|
394
698
|
const rules = ensureArray(route.rules, "Route is missing rules.");
|
|
@@ -400,6 +704,10 @@ export function validateConfigInvariants(config) {
|
|
|
400
704
|
const proxifierIndex = rules.findIndex((rule) => Array.isArray(rule.inbound) &&
|
|
401
705
|
rule.inbound.includes("in-proxifier") &&
|
|
402
706
|
rule.outbound === "Process-Proxy");
|
|
707
|
+
const nativeProcessIndex = rules.findIndex((rule) => rule.outbound === "Process-Proxy" &&
|
|
708
|
+
(Array.isArray(rule.process_name) ||
|
|
709
|
+
Array.isArray(rule.process_path) ||
|
|
710
|
+
Array.isArray(rule.process_path_regex)));
|
|
403
711
|
const stitchIndex = rules.findIndex((rule) => Array.isArray(rule.domain_suffix) &&
|
|
404
712
|
rule.domain_suffix.includes("stitch.withgoogle.com") &&
|
|
405
713
|
rule.outbound === "Stitch-Out");
|
|
@@ -418,7 +726,13 @@ export function validateConfigInvariants(config) {
|
|
|
418
726
|
const priorityOrder = [
|
|
419
727
|
{ name: "quic", index: quicIndex, required: true },
|
|
420
728
|
{ name: "dns", index: dnsIndex, required: true },
|
|
421
|
-
{
|
|
729
|
+
{
|
|
730
|
+
name: "processAware",
|
|
731
|
+
index: nativeProcessIndex >= 0 && proxifierIndex >= 0
|
|
732
|
+
? Math.min(nativeProcessIndex, proxifierIndex)
|
|
733
|
+
: Math.max(nativeProcessIndex, proxifierIndex),
|
|
734
|
+
required: false,
|
|
735
|
+
},
|
|
422
736
|
{ name: "stitch", index: stitchIndex, required: true },
|
|
423
737
|
{ name: "explicitAi", index: explicitAiIndex, required: true },
|
|
424
738
|
{ name: "aiRuleSet", index: aiRuleSetIndex, required: false },
|
|
@@ -435,16 +749,31 @@ export function validateConfigInvariants(config) {
|
|
|
435
749
|
const previous = entries[index - 1];
|
|
436
750
|
return previous !== undefined && previous.index < entry.index;
|
|
437
751
|
});
|
|
438
|
-
checks.push(makeCheck("route-priority", routePriorityPassed, `Rule order indices: quic=${quicIndex}, dns=${dnsIndex}, proxifier=${proxifierIndex}, stitch=${stitchIndex}, explicitAi=${explicitAiIndex}, aiRuleSet=${aiRuleSetIndex}, devRuleSet=${devRuleSetIndex}, china=${chinaIndex}`));
|
|
752
|
+
checks.push(makeCheck("route-priority", routePriorityPassed, `Rule order indices: quic=${quicIndex}, dns=${dnsIndex}, proxifier=${proxifierIndex}, nativeProcess=${nativeProcessIndex}, stitch=${stitchIndex}, explicitAi=${explicitAiIndex}, aiRuleSet=${aiRuleSetIndex}, devRuleSet=${devRuleSetIndex}, china=${chinaIndex}`));
|
|
439
753
|
checks.push(makeCheck("default-domain-resolver", typeof route.default_domain_resolver === "string" &&
|
|
440
754
|
dnsServers.some((server) => server.tag === route.default_domain_resolver), `default_domain_resolver=${String(route.default_domain_resolver)}`));
|
|
441
755
|
checks.push(makeCheck("dns-shape", dnsServers.some((server) => server.type === "local" && server.tag === "dns-local-default") &&
|
|
442
756
|
dnsServers.some((server) => (server.type === "tcp" || server.type === "udp") && server.server === "1.1.1.1") &&
|
|
443
|
-
dnsServers.some((server) => (server.type === "tcp" || server.type === "udp") && server.server === "223.5.5.5")
|
|
757
|
+
dnsServers.some((server) => (server.type === "tcp" || server.type === "udp") && server.server === "223.5.5.5") &&
|
|
758
|
+
(!dnsServers.some((server) => server.type === "fakeip") ||
|
|
759
|
+
(dnsServers.some((server) => server.type === "fakeip" && server.tag === "dns-fakeip") &&
|
|
760
|
+
ensureArray(dns.rules ?? [], "DNS rules are missing.").some((rule) => Array.isArray(rule.query_type) &&
|
|
761
|
+
rule.query_type.includes("A") &&
|
|
762
|
+
rule.server === "dns-fakeip"))), `dns servers=${dnsServers
|
|
444
763
|
.map((server) => `${String(server.tag)}:${String(server.server)}`)
|
|
445
764
|
.join(", ")}`));
|
|
446
765
|
return checks;
|
|
447
766
|
}
|
|
767
|
+
function findLastProcessAwareRuleIndex(rules) {
|
|
768
|
+
return rules.reduce((lastIndex, rule, index) => Array.isArray(rule.process_name) ||
|
|
769
|
+
Array.isArray(rule.process_path) ||
|
|
770
|
+
Array.isArray(rule.process_path_regex)
|
|
771
|
+
? index
|
|
772
|
+
: lastIndex, -1);
|
|
773
|
+
}
|
|
774
|
+
function slugifyProcessName(processName) {
|
|
775
|
+
return processName.replaceAll(/[^A-Za-z0-9._-]+/g, "-");
|
|
776
|
+
}
|
|
448
777
|
export function resolveDefaultLeafOutboundTag(config, tag) {
|
|
449
778
|
const outbounds = ensureArray(config.outbounds, "Config is missing outbounds.");
|
|
450
779
|
const byTag = new Map();
|
|
@@ -481,20 +810,44 @@ export function resolveDefaultLeafOutboundTag(config, tag) {
|
|
|
481
810
|
}
|
|
482
811
|
}
|
|
483
812
|
function buildRuntimeScenarios(config, mixedPort, proxifierPort, configuredScenarios) {
|
|
484
|
-
const sourceScenarios = configuredScenarios && configuredScenarios.length > 0
|
|
813
|
+
const sourceScenarios = normalizeConfiguredVerificationScenarios(config, configuredScenarios && configuredScenarios.length > 0
|
|
485
814
|
? configuredScenarios
|
|
486
|
-
: defaultConfiguredScenarios;
|
|
487
|
-
|
|
815
|
+
: defaultConfiguredScenarios);
|
|
816
|
+
const fallbackScenarios = sourceScenarios.length > 0
|
|
817
|
+
? sourceScenarios
|
|
818
|
+
: normalizeConfiguredVerificationScenarios(config, defaultConfiguredScenarios);
|
|
819
|
+
return fallbackScenarios.map((scenario) => ({
|
|
488
820
|
id: scenario.id,
|
|
489
821
|
name: scenario.name,
|
|
490
822
|
url: scenario.url,
|
|
491
823
|
inboundTag: scenario.inbound,
|
|
492
824
|
proxyPort: scenario.inbound === "in-proxifier" ? proxifierPort : mixedPort,
|
|
493
|
-
expectedOutboundTag:
|
|
494
|
-
? scenario.expectedOutbound
|
|
495
|
-
: resolveDefaultLeafOutboundTag(config, scenario.expectedOutbound),
|
|
825
|
+
expectedOutboundTag: resolveExpectedVerificationOutboundTag(config, scenario.expectedOutbound),
|
|
496
826
|
}));
|
|
497
827
|
}
|
|
828
|
+
export function resolveExpectedVerificationOutboundTag(config, expectedOutboundTag) {
|
|
829
|
+
return expectedOutboundTag === "direct" || expectedOutboundTag === "block"
|
|
830
|
+
? expectedOutboundTag
|
|
831
|
+
: resolveDefaultLeafOutboundTag(config, expectedOutboundTag);
|
|
832
|
+
}
|
|
833
|
+
export function normalizeConfiguredVerificationScenarios(config, scenarios) {
|
|
834
|
+
const availableInbounds = new Set(ensureArray(config.inbounds, "Config is missing inbounds.")
|
|
835
|
+
.map((inbound) => inbound.tag)
|
|
836
|
+
.filter((tag) => typeof tag === "string" && tag.length > 0));
|
|
837
|
+
const hasTun = availableInbounds.has("in-tun");
|
|
838
|
+
const hasMixed = availableInbounds.has("in-mixed");
|
|
839
|
+
const hasProxifier = availableInbounds.has("in-proxifier");
|
|
840
|
+
const prefersTunFallback = hasTun && !hasMixed && !hasProxifier;
|
|
841
|
+
return scenarios.flatMap((scenario) => {
|
|
842
|
+
if (availableInbounds.has(scenario.inbound)) {
|
|
843
|
+
return [scenario];
|
|
844
|
+
}
|
|
845
|
+
if (scenario.inbound === "in-mixed" && (hasTun || prefersTunFallback)) {
|
|
846
|
+
return [{ ...scenario, inbound: "in-tun" }];
|
|
847
|
+
}
|
|
848
|
+
return [];
|
|
849
|
+
});
|
|
850
|
+
}
|
|
498
851
|
const defaultConfiguredScenarios = [
|
|
499
852
|
{
|
|
500
853
|
id: "stitch",
|
|
@@ -551,11 +904,49 @@ async function runProxyRequestScenario(input) {
|
|
|
551
904
|
"5",
|
|
552
905
|
"--proxy",
|
|
553
906
|
`http://127.0.0.1:${input.proxyPort}`,
|
|
907
|
+
"--noproxy",
|
|
908
|
+
"",
|
|
554
909
|
input.url,
|
|
555
910
|
];
|
|
556
911
|
return new Promise((resolve, reject) => {
|
|
557
912
|
const child = spawn(input.requestBinary, args, {
|
|
558
913
|
stdio: ["ignore", "pipe", "pipe"],
|
|
914
|
+
env: buildVerificationRequestEnv(),
|
|
915
|
+
});
|
|
916
|
+
let stdout = "";
|
|
917
|
+
let stderr = "";
|
|
918
|
+
let timedOut = false;
|
|
919
|
+
const timeout = setTimeout(() => {
|
|
920
|
+
timedOut = true;
|
|
921
|
+
child.kill("SIGKILL");
|
|
922
|
+
}, 12_000);
|
|
923
|
+
child.stdout.on("data", (chunk) => {
|
|
924
|
+
stdout += chunk.toString();
|
|
925
|
+
});
|
|
926
|
+
child.stderr.on("data", (chunk) => {
|
|
927
|
+
stderr += chunk.toString();
|
|
928
|
+
});
|
|
929
|
+
child.on("error", (error) => {
|
|
930
|
+
clearTimeout(timeout);
|
|
931
|
+
reject(error);
|
|
932
|
+
});
|
|
933
|
+
child.on("close", (code) => {
|
|
934
|
+
clearTimeout(timeout);
|
|
935
|
+
resolve({
|
|
936
|
+
exitCode: code ?? 0,
|
|
937
|
+
stdout,
|
|
938
|
+
stderr,
|
|
939
|
+
timedOut,
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
async function runNativeProcessProbeScenario(input) {
|
|
945
|
+
const args = ["127.0.0.1", String(input.proxyPort), input.targetHost, String(input.targetPort)];
|
|
946
|
+
return new Promise((resolve, reject) => {
|
|
947
|
+
const child = spawn(input.probeBinaryPath, args, {
|
|
948
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
949
|
+
env: buildVerificationRequestEnv(),
|
|
559
950
|
});
|
|
560
951
|
let stdout = "";
|
|
561
952
|
let stderr = "";
|
|
@@ -600,11 +991,12 @@ async function runJsonRequestScenario(input) {
|
|
|
600
991
|
input.url,
|
|
601
992
|
];
|
|
602
993
|
if (typeof input.proxyPort === "number") {
|
|
603
|
-
args.splice(args.length - 1, 0, "--proxy", `http://127.0.0.1:${input.proxyPort}
|
|
994
|
+
args.splice(args.length - 1, 0, "--proxy", `http://127.0.0.1:${input.proxyPort}`, "--noproxy", "");
|
|
604
995
|
}
|
|
605
996
|
return new Promise((resolve, reject) => {
|
|
606
997
|
const child = spawn(input.requestBinary, args, {
|
|
607
998
|
stdio: ["ignore", "pipe", "pipe"],
|
|
999
|
+
env: buildVerificationRequestEnv(),
|
|
608
1000
|
});
|
|
609
1001
|
let stdout = "";
|
|
610
1002
|
let stderr = "";
|
|
@@ -724,6 +1116,18 @@ async function waitForScenarioLogs(buffer, offset, patterns, timeoutMs) {
|
|
|
724
1116
|
}
|
|
725
1117
|
return undefined;
|
|
726
1118
|
}
|
|
1119
|
+
async function waitForScenarioLogsWithAlternatives(buffer, offset, requiredPatterns, alternativePatterns, timeoutMs) {
|
|
1120
|
+
const startedAt = Date.now();
|
|
1121
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
1122
|
+
const excerpt = buffer.text.slice(offset);
|
|
1123
|
+
if (requiredPatterns.every((pattern) => pattern.test(excerpt)) &&
|
|
1124
|
+
alternativePatterns.some((pattern) => pattern.test(excerpt))) {
|
|
1125
|
+
return excerpt;
|
|
1126
|
+
}
|
|
1127
|
+
await sleep(250);
|
|
1128
|
+
}
|
|
1129
|
+
return undefined;
|
|
1130
|
+
}
|
|
727
1131
|
async function waitForLog(buffer, pattern, timeoutMs, errorMessage) {
|
|
728
1132
|
const startedAt = Date.now();
|
|
729
1133
|
while (Date.now() - startedAt < timeoutMs) {
|
|
@@ -774,6 +1178,124 @@ function makeCheck(name, passed, details) {
|
|
|
774
1178
|
function escapeRegExp(input) {
|
|
775
1179
|
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
776
1180
|
}
|
|
1181
|
+
export function buildVerificationRequestEnv(baseEnv = process.env) {
|
|
1182
|
+
const stripped = {};
|
|
1183
|
+
for (const [key, value] of Object.entries(baseEnv)) {
|
|
1184
|
+
if (/^(http_proxy|https_proxy|all_proxy|no_proxy)$/i.test(key)) {
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
stripped[key] = value;
|
|
1188
|
+
}
|
|
1189
|
+
return stripped;
|
|
1190
|
+
}
|
|
1191
|
+
async function compileNativeProcessProbe(input) {
|
|
1192
|
+
const sourcePath = path.join(input.runDir, "native-process-probe.c");
|
|
1193
|
+
await writeFile(sourcePath, buildNativeProcessProbeSource(), "utf8");
|
|
1194
|
+
await new Promise((resolve, reject) => {
|
|
1195
|
+
const child = spawn(input.compilerBinary, ["-O2", "-Wall", "-Wextra", "-o", input.probeBinaryPath, sourcePath], {
|
|
1196
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1197
|
+
});
|
|
1198
|
+
let stderr = "";
|
|
1199
|
+
child.stdout.on("data", () => {
|
|
1200
|
+
// ignore compiler stdout
|
|
1201
|
+
});
|
|
1202
|
+
child.stderr.on("data", (chunk) => {
|
|
1203
|
+
stderr += chunk.toString();
|
|
1204
|
+
});
|
|
1205
|
+
child.on("error", reject);
|
|
1206
|
+
child.on("close", (code) => {
|
|
1207
|
+
if ((code ?? 0) === 0) {
|
|
1208
|
+
resolve();
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
reject(new Error(`Failed to compile native-process probe with ${input.compilerBinary}.\n${stderr.trim()}`));
|
|
1212
|
+
});
|
|
1213
|
+
});
|
|
1214
|
+
await chmod(input.probeBinaryPath, 0o755);
|
|
1215
|
+
}
|
|
1216
|
+
function buildNativeProcessProbeSource() {
|
|
1217
|
+
return `#include <arpa/inet.h>
|
|
1218
|
+
#include <errno.h>
|
|
1219
|
+
#include <netinet/in.h>
|
|
1220
|
+
#include <stdio.h>
|
|
1221
|
+
#include <stdlib.h>
|
|
1222
|
+
#include <stdint.h>
|
|
1223
|
+
#include <string.h>
|
|
1224
|
+
#include <sys/socket.h>
|
|
1225
|
+
#include <sys/time.h>
|
|
1226
|
+
#include <unistd.h>
|
|
1227
|
+
|
|
1228
|
+
int main(int argc, char **argv) {
|
|
1229
|
+
if (argc != 5) {
|
|
1230
|
+
fprintf(stderr, "usage: %s <proxy-host> <proxy-port> <target-host> <target-port>\\n", argv[0]);
|
|
1231
|
+
return 2;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
const char *proxyHost = argv[1];
|
|
1235
|
+
const char *proxyPort = argv[2];
|
|
1236
|
+
const char *targetHost = argv[3];
|
|
1237
|
+
const char *targetPort = argv[4];
|
|
1238
|
+
|
|
1239
|
+
int socketFd = socket(AF_INET, SOCK_STREAM, 0);
|
|
1240
|
+
if (socketFd < 0) {
|
|
1241
|
+
perror("socket");
|
|
1242
|
+
return 3;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
struct timeval timeout;
|
|
1246
|
+
timeout.tv_sec = 3;
|
|
1247
|
+
timeout.tv_usec = 0;
|
|
1248
|
+
setsockopt(socketFd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
|
1249
|
+
setsockopt(socketFd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
|
|
1250
|
+
|
|
1251
|
+
struct sockaddr_in proxyAddress;
|
|
1252
|
+
memset(&proxyAddress, 0, sizeof(proxyAddress));
|
|
1253
|
+
proxyAddress.sin_family = AF_INET;
|
|
1254
|
+
proxyAddress.sin_port = htons((uint16_t) atoi(proxyPort));
|
|
1255
|
+
|
|
1256
|
+
if (inet_pton(AF_INET, proxyHost, &proxyAddress.sin_addr) != 1) {
|
|
1257
|
+
fprintf(stderr, "invalid proxy host: %s\\n", proxyHost);
|
|
1258
|
+
close(socketFd);
|
|
1259
|
+
return 4;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (connect(socketFd, (struct sockaddr *) &proxyAddress, sizeof(proxyAddress)) != 0) {
|
|
1263
|
+
perror("connect");
|
|
1264
|
+
close(socketFd);
|
|
1265
|
+
return 5;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
char request[1024];
|
|
1269
|
+
int written = snprintf(
|
|
1270
|
+
request,
|
|
1271
|
+
sizeof(request),
|
|
1272
|
+
"CONNECT %s:%s HTTP/1.1\\r\\nHost: %s:%s\\r\\nProxy-Connection: Keep-Alive\\r\\n\\r\\n",
|
|
1273
|
+
targetHost,
|
|
1274
|
+
targetPort,
|
|
1275
|
+
targetHost,
|
|
1276
|
+
targetPort
|
|
1277
|
+
);
|
|
1278
|
+
if (written < 0 || written >= (int) sizeof(request)) {
|
|
1279
|
+
fprintf(stderr, "failed to encode request\\n");
|
|
1280
|
+
close(socketFd);
|
|
1281
|
+
return 6;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
ssize_t sent = send(socketFd, request, (size_t) written, 0);
|
|
1285
|
+
if (sent < 0) {
|
|
1286
|
+
perror("send");
|
|
1287
|
+
close(socketFd);
|
|
1288
|
+
return 7;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
char response[512];
|
|
1292
|
+
recv(socketFd, response, sizeof(response) - 1, 0);
|
|
1293
|
+
usleep(250000);
|
|
1294
|
+
close(socketFd);
|
|
1295
|
+
return 0;
|
|
1296
|
+
}
|
|
1297
|
+
`;
|
|
1298
|
+
}
|
|
777
1299
|
async function isExecutable(filePath) {
|
|
778
1300
|
try {
|
|
779
1301
|
await access(filePath, constants.X_OK);
|