332 lines
9.4 KiB
Markdown
332 lines
9.4 KiB
Markdown
# 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 字符串,解码后的数据布局为:
|
||
|
||
```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_<id>_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_<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");
|
||
}
|
||
```
|
||
|
||
执行:
|
||
|
||
```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_<id>_servers` 字段;
|
||
- 不要把旧版 `~/Library/Preferences/com.MonoCloud.MonoProxyMac.plist` 当作最新数据源;
|
||
- 优先以刚刷新过的 `~/Library/Application Support/MonoProxy/config.json` 为准。
|
||
|
||
### YAML 无法直接连接
|
||
|
||
本文输出刻意删除了 `password`,因此它是用于审阅、比对和更新 `server/port` 的安全快照,并不是可直接连接的完整凭据文件。需要实际连接时,应在本地私密环境中补回自己已有的密码,且不要提交到 Git 或同步到公开笔记库。
|
||
|