mslxdff 0.1.241 → 0.1.242

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.241",
3
+ "version": "0.1.242",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,7 @@ function flagValue(args, ...names) {
25
25
 
26
26
  const kindZh = { "free-quota": "429 额度耗尽", "free-tier": "403 免费层资格", "ip-limit": "429 限流", "black-quota": "账号日配额", network: "网络不可达", none: "非额度信号" };
27
27
  const line = (r) => {
28
- if (r.error) return `✗ 不可达(${String(r.error).slice(0, 60)})`;
28
+ if (r.error) return `✗ 不可达(${String(r.error).replace(/\s+/g, " ").slice(0, 220)})`;
29
29
  if (r.ok) return `✅ HTTP ${r.status} 有额度`;
30
30
  return `⛔ HTTP ${r.status} ${kindZh[r.kind] || r.kind}`;
31
31
  };
@@ -10,7 +10,7 @@ import { genId } from "../opencode-identity.js";
10
10
  import { DEFAULT_PROBE_MODEL, probeUpstreamFrom } from "../v6pool/selftest.js";
11
11
  import { classifyWarpSignal } from "../warp-pool/verdict.js";
12
12
  import { resolveMultiIpConfig, resolveRole } from "./config.js";
13
- import { candidateNodes, loadNodeCache, materializeNode } from "./nodes.js";
13
+ import { candidateNodes, loadNodeCache, materializeNode, writeAuthFile } from "./nodes.js";
14
14
  import { fetchNodes, probeExitIp } from "./nodes-fetch.js";
15
15
  import { bringUpTunnel, checkOpenvpn, isRoot, isSupportedPlatform, tearDownTunnel } from "./tunnel.js";
16
16
  import { fmtExitIp, fmtNodePick, fmtNodes, fmtProbe } from "./report.js";
@@ -79,6 +79,9 @@ export async function runMultiIpCompare({ env = process.env, count = MITEST_DEFA
79
79
 
80
80
  const picked = candidateNodes({ rows, countries: cfg.countries, minScore: cfg.minScore, scan: Math.max(1, Math.min(MITEST_MAX, Number(count) || MITEST_DEFAULT)) });
81
81
  if (!picked.length) return { status: "skip", reason: "没有满足条件的候选节点(国家/Score 过滤后为空)", hint: "放宽 MSLXDFF_MULTI_IP_MIN_SCORE 或去掉 MSLXDFF_MULTI_IP_COUNTRY" };
82
+ // auth 文件必须先落到磁盘再把路径交给 openvpn。取证路径以前把 `deps.authFile`(undefined)直接传下去
83
+ // ⇒ `--auth-user-pass` 拿到空参数 ⇒ openvpn 判「选项缺参数」,只留一句「Use --help」——2026-10-25 真机三条槽全挂在这。
84
+ const authFile = deps.authFile || (deps.writeAuthFile || writeAuthFile)({ dir: cfg.dir, user: cfg.authUser, pass: cfg.authPass });
82
85
 
83
86
  const exits = [];
84
87
  try {
@@ -91,7 +94,7 @@ export async function runMultiIpCompare({ env = process.env, count = MITEST_DEFA
91
94
  try {
92
95
  const node = materialize({ dir: cfg.dir, row });
93
96
  if (!node) throw new Error("节点配置解不开(base64 或 remote 缺失)");
94
- slot = await startSlot({ cfg, slotIndex: i, node, deps: { authFile: deps.authFile, runCmdFn: deps.runCmdFn, spawnFn: deps.spawnFn, wait: deps.wait, log } });
97
+ slot = await startSlot({ cfg, slotIndex: i, node, deps: { authFile, runCmdFn: deps.runCmdFn, spawnFn: deps.spawnFn, wait: deps.wait, log } });
95
98
  const exit = await exitProbe({ agent: agentFactory(slot.tunIp), timeoutMs: cfg.probeTimeoutMs });
96
99
  if (exit.ok) log(fmtExitIp({ dev: slot.dev, ip: exit.ip, ms: exit.ms }));
97
100
  // 从这条隧道的出口打**同一条**免费层请求
@@ -9,7 +9,6 @@
9
9
  // 平台纪律:只有 Linux 能干(tun + `ip rule`);其它平台由调用方**诚实跳过一行**,不做半吊子。
10
10
  import { spawn } from "node:child_process";
11
11
  import { devNameFor, tableFor } from "./config.js";
12
- import { tailReason } from "../warp-pool/revive.js";
13
12
  import { fmtExit, fmtRpf, fmtRuleFail, fmtSpawn, fmtTunAddrFail } from "./report.js";
14
13
 
15
14
  /** openvpn 就绪标志(配置文件里可能出现别的 "Sequence",必须整串匹配)。 */
@@ -39,11 +38,18 @@ export function isRoot(getuid = process.getuid?.bind(process)) {
39
38
  * openvpn 命令行。**dev 与路由一律走命令行覆盖**(命令行优先于 config 文件):
40
39
  * `--route-nopull` + `--pull-filter ignore redirect-gateway` 保证它绝不碰主路由表——
41
40
  * 主路由表被改 = 整台机器的流量(含 daemon 自己打上游)都被拽进隧道,那是灾难。
41
+ * `--dev-type tun` 不可省:槽设备名(`mltun1`…)**不以 tun/tap 开头**,openvpn 从名字推不出类型
42
+ * (其 usage 原文:dev-type「仅当 --dev 的设备名不以 tun 或 tap 开头时使用」);实测 97 个 VPNGate 节点全是 `dev tun`。
43
+ * 三个必填项缺一即抛:空串传下去会被 openvpn 判成「选项缺参数」,只留下一句没头没尾的 usage 错误(2026-10-25 真机踩过)。
42
44
  */
43
45
  export function buildOpenvpnArgs({ conf, dev, authFile, logFile = "", verb = 3 }) {
46
+ for (const [name, v] of [["conf", conf], ["dev", dev], ["authFile", authFile]]) {
47
+ if (!v) throw new Error(`buildOpenvpnArgs 缺 ${name}:拒绝起隧道(空参数会让 openvpn 只报一句 usage 错误,看不出病因)`);
48
+ }
44
49
  return [
45
50
  "--config", conf,
46
51
  "--dev", dev,
52
+ "--dev-type", "tun",
47
53
  "--auth-user-pass", authFile,
48
54
  "--route-nopull",
49
55
  "--pull-filter", "ignore", "redirect-gateway",
@@ -52,6 +58,17 @@ export function buildOpenvpnArgs({ conf, dev, authFile, logFile = "", verb = 3 }
52
58
  ];
53
59
  }
54
60
 
61
+ /**
62
+ * openvpn 输出里挑「最能说明问题」的一行:用法错误的**末行恒是** `Use --help for more information.`,
63
+ * 真凶(`Options error: …`)在它上面。只报末行等于把病因盖住——2026-10-25 真机就因为这样绕了一圈。
64
+ */
65
+ export function openvpnErrorLine(text, max = 240) {
66
+ const lines = String(text || "").split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
67
+ if (!lines.length) return "";
68
+ const pick = lines.find((l) => /options error|unrecognized|missing or extra|fatal|error:|can ?not|cannot|denied|failed|refused|reset|auth failed/i.test(l));
69
+ return (pick || lines[lines.length - 1]).slice(0, max);
70
+ }
71
+
55
72
  /** 从 `ip -4 addr show dev <dev>` 输出里取 tun 地址(VPNGate 推的是 10.x 内网地址)。 */
56
73
  export function parseTunAddr(text) {
57
74
  const m = /inet\s+(\d+\.\d+\.\d+\.\d+)/.exec(String(text || ""));
@@ -139,7 +156,7 @@ export function startTunnel({ bin, args, timeoutMs = 25_000, spawnFn = spawn, lo
139
156
  if (tail.length > 20) tail.shift();
140
157
  }
141
158
  };
142
- const lastTail = () => tailReason(tail.join("\n"));
159
+ const lastTail = () => openvpnErrorLine(tail.join("\n"));
143
160
  const ready = new Promise((resolve, reject) => {
144
161
  const timer = setTimeout(() => {
145
162
  if (settled) return;