# 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__servers` 字段中。该字段是 Base64 字符串,解码后的数据布局为: ```text salt(16 字节) + IV(16 字节) + HMAC-SHA256(32 字节) + AES-256-CBC ciphertext(剩余字节) ``` 密钥派生参数: ```text 算法:PBKDF2-HMAC-SHA256 迭代次数:100000 密钥长度:32 字节 配置封装口令:MonoProxyMac.MNLocalManager.Services.v1 ``` 这里的“配置封装口令”是应用二进制中用于保护本地配置结构的固定值,不是 Shadowsocks 节点的 `password`。 解密流程必须先验证 HMAC,再执行 AES 解密。HMAC 不匹配时应立即停止,不能忽略校验继续处理。 ## 步骤一:让 MonoProxy 刷新本地配置 1. 启动 MonoProxy 并登录自己的账户。 2. 等待节点列表完成刷新;是否开启系统代理不影响离线读取,但刷新过程需要网络。 3. 检查配置文件是否刚刚更新: ```bash stat -f '%N | modified=%Sm | size=%z' \ -t '%Y-%m-%d %H:%M:%S %z' \ "$HOME/Library/Application Support/MonoProxy/config.json" ``` 如果文件不存在,先确认应用是否已登录并成功获取服务信息。 ## 步骤二:确认节点字段 只查看字段名称,不输出令牌或节点密码: ```bash jq -r 'keys[] | select(test("^mn_service_.*_servers$"))' \ "$HOME/Library/Application Support/MonoProxy/config.json" ``` 正常情况下会得到类似: ```text mn_service_2462_servers ``` 服务 ID 可能随账户或后端迁移而变化,因此提取脚本不应硬编码数字部分。 ## 步骤三:使用离线脚本生成无密码 YAML 下面的脚本仅使用 Node.js 内置的 `fs` 和 `crypto` 模块,不需要安装第三方依赖,也不会联网。脚本会: 1. 自动查找 `mn_service__servers` 字段; 2. Base64 解码数据; 3. 使用 PBKDF2-SHA256 派生密钥; 4. 验证 HMAC-SHA256; 5. 使用 AES-256-CBC 解密节点 JSON; 6. 输出不含 `password` 的 Clash YAML。 保存为 `extract-monoproxy.js`: ```javascript 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__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"); } ``` 执行: ```bash node extract-monoproxy.js \ "$HOME/Library/Application Support/MonoProxy/config.json" \ > monoproxy-subscription-without-password.yaml ``` 检查输出中没有密码字段: ```bash rg -n 'password|access_token|refresh_token' \ monoproxy-subscription-without-password.yaml ``` 正常结果应无任何输出。再检查 YAML 的节点数量: ```bash 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 解析均已通过。 ```yaml 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__servers` 字段; - 不要把旧版 `~/Library/Preferences/com.MonoCloud.MonoProxyMac.plist` 当作最新数据源; - 优先以刚刷新过的 `~/Library/Application Support/MonoProxy/config.json` 为准。 ### YAML 无法直接连接 本文输出刻意删除了 `password`,因此它是用于审阅、比对和更新 `server/port` 的安全快照,并不是可直接连接的完整凭据文件。需要实际连接时,应在本地私密环境中补回自己已有的密码,且不要提交到 Git 或同步到公开笔记库。