Files
obsidian-vault/output/MonoProxy订阅信息获取工作流.md
2026-08-31 09:21:12 +08:00

9.4 KiB
Raw Permalink Blame History

MonoProxy 订阅信息获取工作流

本文记录如何从 macOS 版 MonoProxy 的本地配置中获取自有账户的节点信息,并生成不包含节点密码的 Clash YAML 列表。

适用范围

  • 应用路径:/Applications/MonoProxyMac.app
  • 配置路径:~/Library/Application Support/MonoProxy/config.json
  • 已验证日期:2026-07-24
  • 输出字段:name、server、port、type、cipher、udp
  • 明确排除:节点 password、登录令牌、刷新令牌和账户信息

仅应处理自己拥有或获授权访问的账户与配置。不要上传或公开分享原始 config.json,其中还包含账户令牌和加密后的节点密码。

结论

MonoProxy 将节点数组保存在 config.json 的 mn_service_<service-id>_servers 字段中。该字段是 Base64 字符串,解码后的数据布局为:

salt(16 字节)
+ IV(16 字节)
+ HMAC-SHA256(32 字节)
+ AES-256-CBC ciphertext(剩余字节)

密钥派生参数:

算法:PBKDF2-HMAC-SHA256
迭代次数:100000
密钥长度:32 字节
配置封装口令:MonoProxyMac.MNLocalManager.Services.v1

这里的“配置封装口令”是应用二进制中用于保护本地配置结构的固定值,不是 Shadowsocks 节点的 password。

解密流程必须先验证 HMAC,再执行 AES 解密。HMAC 不匹配时应立即停止,不能忽略校验继续处理。

步骤一:让 MonoProxy 刷新本地配置

  1. 启动 MonoProxy 并登录自己的账户。
  2. 等待节点列表完成刷新;是否开启系统代理不影响离线读取,但刷新过程需要网络。
  3. 检查配置文件是否刚刚更新:
stat -f '%N | modified=%Sm | size=%z' \
  -t '%Y-%m-%d %H:%M:%S %z' \
  "$HOME/Library/Application Support/MonoProxy/config.json"

如果文件不存在,先确认应用是否已登录并成功获取服务信息。

步骤二:确认节点字段

只查看字段名称,不输出令牌或节点密码:

jq -r 'keys[] | select(test("^mn_service_.*_servers$"))' \
  "$HOME/Library/Application Support/MonoProxy/config.json"

正常情况下会得到类似:

mn_service_2462_servers

服务 ID 可能随账户或后端迁移而变化,因此提取脚本不应硬编码数字部分。

步骤三:使用离线脚本生成无密码 YAML

下面的脚本仅使用 Node.js 内置的 fs 和 crypto 模块,不需要安装第三方依赖,也不会联网。脚本会:

  1. 自动查找 mn_service_<id>_servers 字段;
  2. Base64 解码数据;
  3. 使用 PBKDF2-SHA256 派生密钥;
  4. 验证 HMAC-SHA256;
  5. 使用 AES-256-CBC 解密节点 JSON;
  6. 输出不含 password 的 Clash YAML。

保存为 extract-monoproxy.js:

const fs = require("fs");
const crypto = require("crypto");

const configPath =
  process.argv[2] ||
  `${process.env.HOME}/Library/Application Support/MonoProxy/config.json`;

const configEnvelopePassword =
  "MonoProxyMac.MNLocalManager.Services.v1";

/**
 * 使用 YAML 单引号格式转义字符串,避免节点名称中的特殊字符破坏 YAML。
 * @param {unknown} value 需要编码的值。
 * @returns {string} 可安全写入 YAML 的单引号字符串。
 */
function yamlString(value) {
  return `'${String(value).replaceAll("'", "''")}'`;
}

const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
const serverKey = Object.keys(config).find((key) =>
  /^mn_service_.*_servers$/.test(key),
);

if (!serverKey) {
  throw new Error("未找到 mn_service_<id>_servers 字段");
}

const envelope = Buffer.from(config[serverKey], "base64");
if (envelope.length <= 64) {
  throw new Error("节点密文长度异常");
}

const salt = envelope.subarray(0, 16);
const iv = envelope.subarray(16, 32);
const storedHmac = envelope.subarray(32, 64);
const ciphertext = envelope.subarray(64);

const key = crypto.pbkdf2Sync(
  configEnvelopePassword,
  salt,
  100000,
  32,
  "sha256",
);

const calculatedHmac = crypto
  .createHmac("sha256", key)
  .update(Buffer.concat([salt, iv, ciphertext]))
  .digest();

if (
  storedHmac.length !== calculatedHmac.length ||
  !crypto.timingSafeEqual(storedHmac, calculatedHmac)
) {
  throw new Error(
    "HMAC 校验失败:配置可能损坏,或 MonoProxy 已更改加密格式",
  );
}

const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
const plaintext = Buffer.concat([
  decipher.update(ciphertext),
  decipher.final(),
]).toString("utf8");

const nodes = JSON.parse(plaintext);
if (!Array.isArray(nodes)) {
  throw new Error("解密结果不是节点数组");
}

console.log("proxies:");
for (const node of nodes) {
  if (!node.alias || !node.hostname || !node.port || !node.encryption) {
    throw new Error("节点缺少 alias/hostname/port/encryption 字段");
  }

  console.log(`  - name: ${yamlString(node.alias)}`);
  console.log(`    server: ${yamlString(node.hostname)}`);
  console.log(`    port: ${Number(node.port)}`);
  console.log("    type: ss");
  console.log(`    cipher: ${yamlString(node.encryption)}`);
  console.log("    udp: true");
}

执行:

node extract-monoproxy.js \
  "$HOME/Library/Application Support/MonoProxy/config.json" \
  > monoproxy-subscription-without-password.yaml

检查输出中没有密码字段:

rg -n 'password|access_token|refresh_token' \
  monoproxy-subscription-without-password.yaml

正常结果应无任何输出。再检查 YAML 的节点数量:

rg -c '^  - name:' monoproxy-subscription-without-password.yaml

当前快照应输出 15。

字段映射

MonoProxy 节点字段 Clash YAML 字段 说明
alias name 节点显示名称
hostname server 节点域名或地址
port port 节点端口
服务类型 Shadowsocks type: ss type 不在每个节点对象中单独存储
encryption cipher 当前均为 chacha20-ietf-poly1305
未单独存储 udp: true 按现有 Clash Shadowsocks 配置补充
password 不输出 本工作流明确排除

当前完整 YAML 快照(不含 password)

数据来自 2026-07-24 15:03:55 更新的本地 config.json。HMAC 校验、AES 解密及 JSON 解析均已通过。

proxies:
  - name: 'Relay-HK1'
    server: 'scott.mydarkcloud.info'
    port: 1904
    type: ss
    cipher: 'chacha20-ietf-poly1305'
    udp: true
  - name: 'Relay-HK2'
    server: 'andrew.mydarkcloud.info'
    port: 2004
    type: ss
    cipher: 'chacha20-ietf-poly1305'
    udp: true
  - name: 'Relay-HK3'
    server: 'ethan.mydarkcloud.info'
    port: 3204
    type: ss
    cipher: 'chacha20-ietf-poly1305'
    udp: true
  - name: 'Relay-HK4'
    server: 'lucas.mydarkcloud.info'
    port: 999
    type: ss
    cipher: 'chacha20-ietf-poly1305'
    udp: true
  - name: 'Relay-SG1'
    server: 'tyler.mydarkcloud.info'
    port: 2604
    type: ss
    cipher: 'chacha20-ietf-poly1305'
    udp: true
  - name: 'Relay-SG2'
    server: 'tyler.mydarkcloud.info'
    port: 2704
    type: ss
    cipher: 'chacha20-ietf-poly1305'
    udp: true
  - name: 'Relay-JP1'
    server: 'patrick.mydarkcloud.info'
    port: 1504
    type: ss
    cipher: 'chacha20-ietf-poly1305'
    udp: true
  - name: 'Relay-JP2'
    server: 'ava.mydarkcloud.info'
    port: 995
    type: ss
    cipher: 'chacha20-ietf-poly1305'
    udp: true
  - name: 'Relay-TW1'
    server: 'kevin.mydarkcloud.info'
    port: 2104
    type: ss
    cipher: 'chacha20-ietf-poly1305'
    udp: true
  - name: 'Relay-TW2'
    server: 'kevin.mydarkcloud.info'
    port: 2204
    type: ss
    cipher: 'chacha20-ietf-poly1305'
    udp: true
  - name: 'Relay-US1'
    server: 'nathan.mydarkcloud.info'
    port: 1204
    type: ss
    cipher: 'chacha20-ietf-poly1305'
    udp: true
  - name: 'Relay-US2'
    server: 'nathan.mydarkcloud.info'
    port: 1304
    type: ss
    cipher: 'chacha20-ietf-poly1305'
    udp: true
  - name: 'JP3'
    server: 'noah.mydarkcloud.info'
    port: 999
    type: ss
    cipher: 'chacha20-ietf-poly1305'
    udp: true
  - name: 'TW1'
    server: 'tw1.mydarkcloud.info'
    port: 999
    type: ss
    cipher: 'chacha20-ietf-poly1305'
    udp: true
  - name: 'TR1'
    server: 'tr1.mydarkcloud.info'
    port: 999
    type: ss
    cipher: 'chacha20-ietf-poly1305'
    udp: true

验证与故障处理

HMAC 校验失败

不要跳过校验。常见原因:

  • config.json 正在被应用写入,读取到了不完整内容;
  • MonoProxy 更新后更改了封装口令、迭代次数或加密格式;
  • 读取了其他应用或旧版本生成的配置文件。

先等待应用完成刷新并重新执行。如果仍失败,需要重新检查当前二进制中的:

  • MNLocalManager -_encryptedJSONObjectForKey:
  • Ctor +d:p:e:
  • CCKeyDerivationPBKDF 参数
  • AES256CBCDecryptData:key:iv:error:
  • HMACSHA256WithData:key:

输出节点为空或字段缺失

  • 确认账户仍有有效服务;
  • 确认找到的是当前 mn_service_<id>_servers 字段;
  • 不要把旧版 ~/Library/Preferences/com.MonoCloud.MonoProxyMac.plist 当作最新数据源;
  • 优先以刚刷新过的 ~/Library/Application Support/MonoProxy/config.json 为准。

YAML 无法直接连接

本文输出刻意删除了 password,因此它是用于审阅、比对和更新 server/port 的安全快照,并不是可直接连接的完整凭据文件。需要实际连接时,应在本地私密环境中补回自己已有的密码,且不要提交到 Git 或同步到公开笔记库。