guanghulab/server/proxy/service/subscription-server.js
2026-05-10 13:12:44 +08:00

1189 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// ═══════════════════════════════════════════════
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
// 📜 Copyright: 国作登字-2026-A-00037559
// ═══════════════════════════════════════════════
// server/proxy/service/subscription-server.js
// 🌐 铸渊专线V1 · 订阅服务
//
// ⚠️ [DEPRECATED · D58] V1节点已停用
// 铸渊专线2.0已测试通过并正式启用V1节点不再使用。
// V2订阅服务: subscription-server-v2.js (多用户独立专线·共享流量池2000GB/月)
// 此文件保留作为V1历史参考不再部署运行。
//
// 提供HTTP端点客户端通过订阅URL获取代理配置
// 自动识别客户端类型,返回对应格式:
// - Clash YAML (Clash Verge / ClashMi)
// - Base64 URI (Shadowrocket)
//
// 端口: 3802 (绑定127.0.0.1仅通过Nginx反代访问)
// 认证: URL中的token参数
//
// 环境变量 (从 /opt/zhuyuan/proxy/.env.keys 加载):
// ZY_PROXY_UUID, ZY_PROXY_REALITY_PUBLIC_KEY,
// ZY_PROXY_REALITY_SHORT_ID, ZY_PROXY_SUB_TOKEN
// ═══════════════════════════════════════════════
'use strict';
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
const os = require('os');
const PORT = process.env.ZY_PROXY_SUB_PORT || 3802;
const DATA_DIR = process.env.ZY_PROXY_DATA_DIR || '/opt/zhuyuan/proxy/data';
const KEYS_FILE = process.env.ZY_PROXY_KEYS_FILE || '/opt/zhuyuan/proxy/.env.keys';
// ── 加载密钥 ────────────────────────────────
function loadKeys() {
const keys = {};
try {
const content = fs.readFileSync(KEYS_FILE, 'utf8');
for (const line of content.split('\n')) {
if (line.startsWith('#') || !line.includes('=')) continue;
const [key, ...vals] = line.split('=');
keys[key.trim()] = vals.join('=').trim();
}
} catch (err) {
// 尝试从环境变量读取
keys.ZY_PROXY_UUID = process.env.ZY_PROXY_UUID || '';
keys.ZY_PROXY_REALITY_PUBLIC_KEY = process.env.ZY_PROXY_REALITY_PUBLIC_KEY || '';
keys.ZY_PROXY_REALITY_SHORT_ID = process.env.ZY_PROXY_REALITY_SHORT_ID || '';
keys.ZY_PROXY_SUB_TOKEN = process.env.ZY_PROXY_SUB_TOKEN || '';
}
return keys;
}
// ── 获取服务器IP ────────────────────────────
// ⚠️ 仓库公开不在代码中硬编码IP
// 优先级: 环境变量 > .env.keys文件 > 回退
function getServerHost() {
// 1. 优先从环境变量读取
if (process.env.ZY_SERVER_HOST) {
return process.env.ZY_SERVER_HOST;
}
// 2. 从.env.keys文件读取
try {
const content = fs.readFileSync(KEYS_FILE, 'utf8');
for (const line of content.split('\n')) {
if (line.startsWith('#') || !line.includes('=')) continue;
const [key, ...vals] = line.split('=');
if (key.trim() === 'ZY_SERVER_HOST') {
const val = vals.join('=').trim();
if (val) return val;
}
}
} catch (err) {
if (err.code !== 'ENOENT') {
console.error(`⚠️ 读取 ${KEYS_FILE} 失败: ${err.code || err.message}`);
}
}
console.error('⚠️ ZY_SERVER_HOST 未设置 (环境变量和.env.keys均未找到)');
return '0.0.0.0';
}
// ── 获取CN中转服务器信息 ─────────────────────
// 优先级: 环境变量 > .env.keys文件
function getCnRelayHost() {
// 1. 从环境变量读取
if (process.env.ZY_CN_RELAY_HOST) {
return process.env.ZY_CN_RELAY_HOST;
}
// 2. 从.env.keys文件读取
try {
const content = fs.readFileSync(KEYS_FILE, 'utf8');
for (const line of content.split('\n')) {
if (line.startsWith('#') || !line.includes('=')) continue;
const [key, ...vals] = line.split('=');
if (key.trim() === 'ZY_CN_RELAY_HOST') {
const val = vals.join('=').trim();
if (val) return val;
}
}
} catch (err) { /* ignore */ }
return null; // CN中转未配置
}
function getCnRelayPort() {
return parseInt(process.env.ZY_CN_RELAY_PORT || '2053', 10);
}
// ── 读取流量配额信息 ────────────────────────
function getQuotaInfo() {
const quotaFile = path.join(DATA_DIR, 'quota-status.json');
try {
return JSON.parse(fs.readFileSync(quotaFile, 'utf8'));
} catch {
return {
total_bytes: 500 * 1024 * 1024 * 1024, // 500GB
used_bytes: 0,
upload_bytes: 0,
download_bytes: 0,
reset_day: 1,
period: new Date().toISOString().slice(0, 7)
};
}
}
// ── 生成VLESS URI (Shadowrocket) ─────────────
function generateVlessUri(keys, serverHost) {
const params = new URLSearchParams({
encryption: 'none',
flow: 'xtls-rprx-vision',
security: 'reality',
sni: 'www.microsoft.com',
fp: 'chrome',
pbk: keys.ZY_PROXY_REALITY_PUBLIC_KEY,
sid: keys.ZY_PROXY_REALITY_SHORT_ID,
type: 'tcp',
headerType: 'none'
});
return `vless://${keys.ZY_PROXY_UUID}@${serverHost}:443?${params.toString()}#ZY-SG-Reality`;
}
// ── 生成Clash YAML配置 ───────────────────────
// v3.0 · 完整Mihomo/Clash Meta兼容配置
// 必须包含: DNS fake-ip + 全局设置 + sniffer + GeoData
// 缺少DNS配置会导致Clash流量为0 (DNS查询绕过代理)
function generateClashYaml(keys, serverHost) {
const cnRelayHost = getCnRelayHost();
const cnRelayPort = getCnRelayPort();
// CN中转节点 (如果已配置)
// 注: CN中转是透明TCP转发(CN:2053 → SG:443)所以Reality设置仍指向SG的配置
// servername/public-key/short-id 与SG直连节点完全相同因为TLS握手实际发生在SG端
const cnProxyBlock = cnRelayHost ? `
- name: "🇨🇳 铸渊专线-CN中转"
type: vless
server: ${cnRelayHost}
port: ${cnRelayPort}
uuid: ${keys.ZY_PROXY_UUID}
network: tcp
tls: true
udp: true
flow: xtls-rprx-vision
skip-cert-verify: false
servername: www.microsoft.com
reality-opts:
public-key: ${keys.ZY_PROXY_REALITY_PUBLIC_KEY}
short-id: ${keys.ZY_PROXY_REALITY_SHORT_ID}
client-fingerprint: chrome` : '';
// 代理组中的节点列表
// SG直连优先 (更可靠的直连节点排在前面作为默认)
const proxyList = cnRelayHost
? ` - "🏛️ 铸渊专线-SG直连"
- "🇨🇳 铸渊专线-CN中转"`
: ' - "🏛️ 铸渊专线-SG直连"';
// 自动选择组 (url-test: 自动测试延迟,选择最快可用节点)
// CN中转如果不可用(connection refused)会自动被排除
const autoGroupBlock = cnRelayHost ? `
- name: "♻️ 自动选择"
type: url-test
proxies:
- "🏛️ 铸渊专线-SG直连"
- "🇨🇳 铸渊专线-CN中转"
url: "http://www.gstatic.com/generate_204"
interval: 300
tolerance: 50
` : '';
// 主代理组: 有CN时默认使用自动选择无CN时直接使用SG
const mainGroupProxies = cnRelayHost
? ` - "♻️ 自动选择"
- "🏛️ 铸渊专线-SG直连"
- "🇨🇳 铸渊专线-CN中转"
- DIRECT`
: ` - "🏛️ 铸渊专线-SG直连"
- DIRECT`;
return `# 铸渊专线 · ZY-Proxy Subscription
# 自动生成 · ${new Date().toISOString()}
# ⚠️ 请勿分享此配置
# 配置版本: v3.0 (Mihomo/Clash Meta 完整兼容)
${cnRelayHost ? `# 🇨🇳 包含CN中转节点 (国内直连广州→转发新加坡)` : ''}
# ── 全局设置 ──────────────────────────────
mixed-port: 7890
allow-lan: false
mode: rule
log-level: info
ipv6: false
unified-delay: true
tcp-concurrent: true
find-process-mode: strict
geodata-mode: true
geodata-loader: standard
global-client-fingerprint: chrome
keep-alive-interval: 30
external-controller: 127.0.0.1:9090
# ── GeoData 数据源 ────────────────────────
geox-url:
geoip: "https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat"
geosite: "https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat"
mmdb: "https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/country.mmdb"
# ── DNS 设置 (fake-ip模式·必须) ───────────
# 缺少此配置会导致DNS查询绕过代理流量显示0
dns:
enable: true
listen: 0.0.0.0:1053
ipv6: false
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
fake-ip-filter:
- "*.lan"
- "*.local"
- "*.direct"
- "localhost.ptlogin2.qq.com"
- "dns.msftncsi.com"
- "*.msftconnecttest.com"
- "*.msftncsi.com"
- "+.stun.*.*"
- "+.stun.*.*.*"
- "+.stun.*.*.*.*"
- "+.stun.*.*.*.*.*"
- "lens.l.google.com"
- "stun.l.google.com"
- "time.*.com"
- "time.*.gov"
- "time.*.edu.cn"
- "time.*.apple.com"
- "time-ios.apple.com"
- "time-macos.apple.com"
- "time1.*.com"
- "ntp.*.com"
- "+.pool.ntp.org"
- "music.163.com"
- "*.music.163.com"
- "*.126.net"
- "*.ntp.org.cn"
default-nameserver:
- 223.5.5.5
- 119.29.29.29
- 1.0.0.1
nameserver:
- https://dns.alidns.com/dns-query
- https://doh.pub/dns-query
fallback:
- https://1.0.0.1/dns-query
- https://dns.google/dns-query
- tls://8.8.4.4:853
fallback-filter:
geoip: true
geoip-code: CN
geosite:
- gfw
ipcidr:
- 240.0.0.0/4
domain:
- "+.google.com"
- "+.google.com.hk"
- "+.facebook.com"
- "+.youtube.com"
- "+.github.com"
- "+.googleapis.com"
- "+.openai.com"
- "+.anthropic.com"
# ── 域名嗅探 ──────────────────────────────
sniffer:
enable: true
force-dns-mapping: true
parse-pure-ip: true
override-destination: true
sniff:
HTTP:
ports: [80, 8080-8880]
override-destination: true
TLS:
ports: [443, 8443]
QUIC:
ports: [443, 8443]
skip-domain:
- "Mijia Cloud"
- "+.push.apple.com"
# ── 代理节点 ──────────────────────────────
proxies:
- name: "🏛️ 铸渊专线-SG直连"
type: vless
server: ${serverHost}
port: 443
uuid: ${keys.ZY_PROXY_UUID}
network: tcp
tls: true
udp: true
flow: xtls-rprx-vision
skip-cert-verify: false
servername: www.microsoft.com
reality-opts:
public-key: ${keys.ZY_PROXY_REALITY_PUBLIC_KEY}
short-id: ${keys.ZY_PROXY_REALITY_SHORT_ID}
client-fingerprint: chrome
${cnProxyBlock}
# ── 代理组 ────────────────────────────────
proxy-groups:
- name: "🌐 铸渊专线"
type: select
proxies:
${mainGroupProxies}
${autoGroupBlock}
- name: "🤖 AI服务"
type: select
proxies:
${proxyList}
- name: "💻 开发工具"
type: select
proxies:
${proxyList}
# ── 路由规则 ──────────────────────────────
rules:
# AI服务
- DOMAIN-SUFFIX,openai.com,🤖 AI服务
- DOMAIN-SUFFIX,anthropic.com,🤖 AI服务
- DOMAIN-SUFFIX,claude.ai,🤖 AI服务
- DOMAIN-SUFFIX,chatgpt.com,🤖 AI服务
- DOMAIN-SUFFIX,bard.google.com,🤖 AI服务
- DOMAIN-SUFFIX,gemini.google.com,🤖 AI服务
- DOMAIN-SUFFIX,ai.google.dev,🤖 AI服务
- DOMAIN-SUFFIX,perplexity.ai,🤖 AI服务
- DOMAIN-SUFFIX,poe.com,🤖 AI服务
# 开发工具
- DOMAIN-SUFFIX,github.com,💻 开发工具
- DOMAIN-SUFFIX,githubusercontent.com,💻 开发工具
- DOMAIN-SUFFIX,github.io,💻 开发工具
- DOMAIN-SUFFIX,githubassets.com,💻 开发工具
- DOMAIN-SUFFIX,copilot.microsoft.com,💻 开发工具
- DOMAIN-SUFFIX,npmjs.com,💻 开发工具
- DOMAIN-SUFFIX,npmjs.org,💻 开发工具
- DOMAIN-SUFFIX,docker.com,💻 开发工具
- DOMAIN-SUFFIX,docker.io,💻 开发工具
- DOMAIN-SUFFIX,stackoverflow.com,💻 开发工具
- DOMAIN-SUFFIX,stackexchange.com,💻 开发工具
- DOMAIN-SUFFIX,vercel.app,💻 开发工具
- DOMAIN-SUFFIX,netlify.app,💻 开发工具
- DOMAIN-SUFFIX,pypi.org,💻 开发工具
# 社交媒体 & 流媒体
- DOMAIN-SUFFIX,tiktok.com,🌐 铸渊专线
- DOMAIN-SUFFIX,twitter.com,🌐 铸渊专线
- DOMAIN-SUFFIX,x.com,🌐 铸渊专线
- DOMAIN-SUFFIX,twimg.com,🌐 铸渊专线
- DOMAIN-SUFFIX,youtube.com,🌐 铸渊专线
- DOMAIN-SUFFIX,ytimg.com,🌐 铸渊专线
- DOMAIN-SUFFIX,yt.be,🌐 铸渊专线
- DOMAIN-SUFFIX,googlevideo.com,🌐 铸渊专线
- DOMAIN-SUFFIX,google.com,🌐 铸渊专线
- DOMAIN-SUFFIX,googleapis.com,🌐 铸渊专线
- DOMAIN-SUFFIX,gstatic.com,🌐 铸渊专线
- DOMAIN-SUFFIX,ggpht.com,🌐 铸渊专线
- DOMAIN-SUFFIX,telegram.org,🌐 铸渊专线
- DOMAIN-SUFFIX,t.me,🌐 铸渊专线
- DOMAIN-SUFFIX,telesco.pe,🌐 铸渊专线
- DOMAIN-SUFFIX,instagram.com,🌐 铸渊专线
- DOMAIN-SUFFIX,cdninstagram.com,🌐 铸渊专线
- DOMAIN-SUFFIX,facebook.com,🌐 铸渊专线
- DOMAIN-SUFFIX,fbcdn.net,🌐 铸渊专线
- DOMAIN-SUFFIX,whatsapp.com,🌐 铸渊专线
- DOMAIN-SUFFIX,whatsapp.net,🌐 铸渊专线
- DOMAIN-SUFFIX,wikipedia.org,🌐 铸渊专线
- DOMAIN-SUFFIX,wikimedia.org,🌐 铸渊专线
- DOMAIN-SUFFIX,reddit.com,🌐 铸渊专线
- DOMAIN-SUFFIX,redd.it,🌐 铸渊专线
- DOMAIN-SUFFIX,redditstatic.com,🌐 铸渊专线
- DOMAIN-SUFFIX,netflix.com,🌐 铸渊专线
- DOMAIN-SUFFIX,nflxvideo.net,🌐 铸渊专线
- DOMAIN-SUFFIX,spotify.com,🌐 铸渊专线
- DOMAIN-SUFFIX,discord.com,🌐 铸渊专线
- DOMAIN-SUFFIX,discord.gg,🌐 铸渊专线
- DOMAIN-SUFFIX,discordapp.com,🌐 铸渊专线
# Apple 服务
- DOMAIN-SUFFIX,apple.com,DIRECT
- DOMAIN-SUFFIX,icloud.com,DIRECT
- DOMAIN-SUFFIX,mzstatic.com,DIRECT
- DOMAIN-KEYWORD,apple,DIRECT
- DOMAIN-KEYWORD,icloud,DIRECT
# 国内常用直连
- DOMAIN-SUFFIX,cn,DIRECT
- DOMAIN-SUFFIX,npmmirror.com,DIRECT
- DOMAIN-SUFFIX,taobao.com,DIRECT
- DOMAIN-SUFFIX,tmall.com,DIRECT
- DOMAIN-SUFFIX,alipay.com,DIRECT
- DOMAIN-SUFFIX,alibaba.com,DIRECT
- DOMAIN-SUFFIX,aliyun.com,DIRECT
- DOMAIN-SUFFIX,aliyuncs.com,DIRECT
- DOMAIN-SUFFIX,jd.com,DIRECT
- DOMAIN-SUFFIX,qq.com,DIRECT
- DOMAIN-SUFFIX,tencent.com,DIRECT
- DOMAIN-SUFFIX,weixin.qq.com,DIRECT
- DOMAIN-SUFFIX,bilibili.com,DIRECT
- DOMAIN-SUFFIX,163.com,DIRECT
- DOMAIN-SUFFIX,126.net,DIRECT
- DOMAIN-SUFFIX,baidu.com,DIRECT
- DOMAIN-SUFFIX,bdstatic.com,DIRECT
- DOMAIN-SUFFIX,zhihu.com,DIRECT
- DOMAIN-SUFFIX,douyin.com,DIRECT
- DOMAIN-SUFFIX,doubanio.com,DIRECT
- DOMAIN-SUFFIX,weibo.com,DIRECT
- DOMAIN-SUFFIX,xiaomi.com,DIRECT
- DOMAIN-SUFFIX,meituan.com,DIRECT
- DOMAIN-SUFFIX,dianping.com,DIRECT
- DOMAIN-KEYWORD,baidu,DIRECT
- DOMAIN-KEYWORD,alibaba,DIRECT
# 局域网直连
- IP-CIDR,192.168.0.0/16,DIRECT
- IP-CIDR,10.0.0.0/8,DIRECT
- IP-CIDR,172.16.0.0/12,DIRECT
- IP-CIDR,127.0.0.0/8,DIRECT
- IP-CIDR,100.64.0.0/10,DIRECT
# GeoIP中国直连
- GEOIP,CN,DIRECT
# 默认走代理
- MATCH,🌐 铸渊专线
`;
}
// ── 生成subscription-userinfo头 ──────────────
function generateUserInfoHeader(quota) {
// 标准格式: upload=BYTES; download=BYTES; total=BYTES; expire=TIMESTAMP
const nextMonth = new Date();
nextMonth.setMonth(nextMonth.getMonth() + 1);
nextMonth.setDate(quota.reset_day || 1);
nextMonth.setHours(0, 0, 0, 0);
return `upload=${quota.upload_bytes}; download=${quota.download_bytes}; total=${quota.total_bytes}; expire=${Math.floor(nextMonth.getTime() / 1000)}`;
}
// ── 生成sing-box客户端配置 (JSON) ────────────
// 铸渊专线客户端专用格式供Flutter/sing-box引擎使用
function generateSingboxConfig(keys, serverHost) {
const cnRelayHost = getCnRelayHost();
const cnRelayPort = getCnRelayPort();
// 出站节点列表
const outbounds = [
{
tag: 'zy-sg-direct',
type: 'vless',
server: serverHost,
server_port: 443,
uuid: keys.ZY_PROXY_UUID,
flow: 'xtls-rprx-vision',
tls: {
enabled: true,
server_name: 'www.microsoft.com',
utls: { enabled: true, fingerprint: 'chrome' },
reality: {
enabled: true,
public_key: keys.ZY_PROXY_REALITY_PUBLIC_KEY,
short_id: keys.ZY_PROXY_REALITY_SHORT_ID
}
},
packet_encoding: 'xudp'
}
];
// CN中转节点 (如果已配置)
if (cnRelayHost) {
outbounds.push({
tag: 'zy-cn-relay',
type: 'vless',
server: cnRelayHost,
server_port: cnRelayPort,
uuid: keys.ZY_PROXY_UUID,
flow: 'xtls-rprx-vision',
tls: {
enabled: true,
server_name: 'www.microsoft.com',
utls: { enabled: true, fingerprint: 'chrome' },
reality: {
enabled: true,
public_key: keys.ZY_PROXY_REALITY_PUBLIC_KEY,
short_id: keys.ZY_PROXY_REALITY_SHORT_ID
}
},
packet_encoding: 'xudp'
});
}
// 自动选择出站
const autoOutboundTags = ['zy-sg-direct'];
if (cnRelayHost) autoOutboundTags.push('zy-cn-relay');
outbounds.push(
{
tag: 'auto',
type: 'urltest',
outbounds: [...autoOutboundTags],
url: 'http://www.gstatic.com/generate_204',
interval: '5m',
tolerance: 50
},
{
tag: 'proxy',
type: 'selector',
outbounds: ['auto', ...autoOutboundTags, 'direct'],
default: 'auto'
},
{
tag: 'ai-services',
type: 'selector',
outbounds: [...autoOutboundTags],
default: autoOutboundTags[0]
},
{
tag: 'dev-tools',
type: 'selector',
outbounds: [...autoOutboundTags],
default: autoOutboundTags[0]
},
{ tag: 'direct', type: 'direct' },
{ tag: 'block', type: 'block' },
{ tag: 'dns-out', type: 'dns' }
);
// DNS配置 (fake-ip模式)
const dns = {
servers: [
{
tag: 'remote-dns',
address: 'https://dns.google/dns-query',
address_resolver: 'local-dns',
detour: 'proxy'
},
{
tag: 'local-dns',
address: 'https://dns.alidns.com/dns-query',
detour: 'direct'
},
{
tag: 'fakeip-dns',
address: 'fakeip'
},
{
tag: 'block-dns',
address: 'rcode://success'
}
],
rules: [
{ outbound: 'any', server: 'local-dns' },
{
rule_set: 'geosite-cn',
server: 'local-dns'
},
{
query_type: ['A', 'AAAA'],
server: 'fakeip-dns'
}
],
fakeip: {
enabled: true,
inet4_range: '198.18.0.0/15',
inet6_range: 'fc00::/18'
},
independent_cache: true
};
// 路由规则
const route = {
rule_set: [
{
tag: 'geosite-cn',
type: 'remote',
format: 'binary',
url: 'https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs',
download_detour: 'proxy'
},
{
tag: 'geoip-cn',
type: 'remote',
format: 'binary',
url: 'https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs',
download_detour: 'proxy'
}
],
rules: [
{ protocol: 'dns', outbound: 'dns-out' },
// AI服务走专用出站
{
domain_suffix: [
'openai.com', 'anthropic.com', 'claude.ai', 'chatgpt.com',
'bard.google.com', 'gemini.google.com', 'ai.google.dev',
'perplexity.ai', 'poe.com'
],
outbound: 'ai-services'
},
// 开发工具走专用出站
{
domain_suffix: [
'github.com', 'githubusercontent.com', 'github.io', 'githubassets.com',
'copilot.microsoft.com', 'npmjs.com', 'npmjs.org',
'docker.com', 'docker.io', 'stackoverflow.com', 'stackexchange.com',
'vercel.app', 'netlify.app', 'pypi.org'
],
outbound: 'dev-tools'
},
// 国内直连
{ rule_set: 'geosite-cn', outbound: 'direct' },
{ rule_set: 'geoip-cn', outbound: 'direct' },
{ ip_is_private: true, outbound: 'direct' }
],
auto_detect_interface: true,
final: 'proxy'
};
// 入站配置
const inbounds = [
{
tag: 'tun-in',
type: 'tun',
inet4_address: '172.19.0.1/30',
inet6_address: 'fdfe:dcba:9876::1/126',
auto_route: true,
strict_route: true,
stack: 'system',
sniff: true,
sniff_override_destination: true
},
{
tag: 'mixed-in',
type: 'mixed',
listen: '127.0.0.1',
listen_port: 7890,
sniff: true,
sniff_override_destination: true
}
];
return {
log: { level: 'info', timestamp: true },
dns,
inbounds,
outbounds,
route,
experimental: {
cache_file: { enabled: true },
clash_api: {
external_controller: '127.0.0.1:9090',
secret: ''
}
},
_metadata: {
generator: 'zy-proxy-subscription',
version: '1.0.0',
generated_at: new Date().toISOString(),
copyright: '国作登字-2026-A-00037559'
}
};
}
// ── 构建节点信息列表 ─────────────────────────
function buildNodeList(keys, serverHost) {
const cnRelayHost = getCnRelayHost();
const cnRelayPort = getCnRelayPort();
const nodes = [
{
id: 'zy-sg-direct',
name: '🏛️ 铸渊专线-SG直连',
region: 'sg',
region_name: 'Singapore',
server: serverHost,
port: 443,
protocol: 'vless',
security: 'reality',
flow: 'xtls-rprx-vision',
sni: 'www.microsoft.com',
fingerprint: 'chrome',
available: true,
priority: 1
}
];
if (cnRelayHost) {
nodes.push({
id: 'zy-cn-relay',
name: '🇨🇳 铸渊专线-CN中转',
region: 'cn',
region_name: 'China (Relay)',
server: cnRelayHost,
port: cnRelayPort,
protocol: 'vless',
security: 'reality',
flow: 'xtls-rprx-vision',
sni: 'www.microsoft.com',
fingerprint: 'chrome',
available: true,
priority: 2
});
}
return nodes;
}
// ── 读取守护状态 ─────────────────────────────
function readGuardianStatus() {
const guardianFile = path.join(DATA_DIR, 'guardian-status.json');
try {
return JSON.parse(fs.readFileSync(guardianFile, 'utf8'));
} catch {
return null;
}
}
// ── 常见错误码中文翻译表 ─────────────────────
const ERROR_TRANSLATIONS = {
'i/o timeout': {
zh: '服务器暂时无法连接',
suggestion: '请切换到CN中转节点或检查本地网络'
},
'connection refused': {
zh: '服务器拒绝连接',
suggestion: '请检查服务器是否在运行,或稍后重试'
},
'TLS handshake': {
zh: '加密握手失败',
suggestion: '网络环境可能受限请切换节点或尝试CN中转'
},
'context deadline exceeded': {
zh: '连接超时',
suggestion: '网络延迟过高,建议切换节点或检查本地网络'
},
'no route to host': {
zh: '无法到达服务器',
suggestion: '可能是网络中断或IP被封锁请切换节点'
},
'connection reset': {
zh: '连接被重置',
suggestion: '可能被防火墙干扰建议切换到CN中转节点'
},
'EOF': {
zh: '连接意外断开',
suggestion: '请检查网络稳定性或切换节点重试'
},
'certificate': {
zh: 'TLS证书验证失败',
suggestion: '请检查系统时间是否正确,或更新客户端'
}
};
// ── 检测客户端类型 ───────────────────────────
function detectClientType(userAgent) {
const ua = (userAgent || '').toLowerCase();
if (ua.includes('clash') || ua.includes('mihomo') || ua.includes('stash')) {
return 'clash';
}
if (ua.includes('shadowrocket') || ua.includes('quantumult') || ua.includes('surge')) {
return 'base64';
}
// 默认返回Clash格式 (最通用)
return 'clash';
}
// ── HTTP服务器 ───────────────────────────────
const server = http.createServer((req, res) => {
try {
const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname;
// 健康检查
if (pathname === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', service: 'zy-proxy-subscription' }));
return;
}
// 订阅端点: /sub/{token}
const subMatch = pathname.match(/^\/sub\/([a-f0-9]+)$/);
if (subMatch) {
const token = subMatch[1];
const keys = loadKeys();
// 验证Token
if (token !== keys.ZY_PROXY_SUB_TOKEN) {
res.writeHead(403, { 'Content-Type': 'text/plain' });
res.end('Forbidden');
return;
}
const serverHost = getServerHost();
const quota = getQuotaInfo();
const clientType = detectClientType(req.headers['user-agent']);
const userInfoHeader = generateUserInfoHeader(quota);
if (clientType === 'clash') {
// Clash YAML格式
const yaml = generateClashYaml(keys, serverHost);
res.writeHead(200, {
'Content-Type': 'text/yaml; charset=utf-8',
'Content-Disposition': 'attachment; filename="zy-proxy.yaml"',
'subscription-userinfo': userInfoHeader,
'profile-update-interval': '6',
'profile-title': 'base64:6ZO45ria5LiT57q/', // "铸渊专线" in base64
});
res.end(yaml);
} else {
// Base64 URI格式 (Shadowrocket)
const vlessUri = generateVlessUri(keys, serverHost);
const encoded = Buffer.from(vlessUri).toString('base64');
res.writeHead(200, {
'Content-Type': 'text/plain; charset=utf-8',
'subscription-userinfo': userInfoHeader,
'profile-update-interval': '6',
});
res.end(encoded);
}
return;
}
// 配额查询端点: /quota (公开安全 - 仅数字)
if (pathname === '/quota') {
const quota = getQuotaInfo();
const totalGB = (quota.total_bytes / (1024 ** 3)).toFixed(1);
const usedGB = ((quota.upload_bytes + quota.download_bytes) / (1024 ** 3)).toFixed(1);
const remainGB = (totalGB - usedGB).toFixed(1);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
total_gb: parseFloat(totalGB),
used_gb: parseFloat(usedGB),
remaining_gb: parseFloat(remainGB),
percentage_used: parseFloat(((usedGB / totalGB) * 100).toFixed(1)),
period: quota.period,
reset_day: quota.reset_day,
updated_at: quota.updated_at || new Date().toISOString()
}));
return;
}
// ── 铸渊客户端API端点 ─────────────────────
// 以下端点需要token认证供铸渊专线专属客户端使用
// 客户端配置端点: /client-config/{token}
// 返回sing-box JSON格式配置
const clientConfigMatch = pathname.match(/^\/client-config\/([a-f0-9]+)$/);
if (clientConfigMatch) {
const token = clientConfigMatch[1];
const keys = loadKeys();
if (token !== keys.ZY_PROXY_SUB_TOKEN) {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: true, code: 'FORBIDDEN', message: '认证失败' }));
return;
}
const serverHost = getServerHost();
const config = generateSingboxConfig(keys, serverHost);
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(config, null, 2));
return;
}
// 节点列表端点: /nodes/{token}
// 返回所有可用节点的结构化信息
const nodesMatch = pathname.match(/^\/nodes\/([a-f0-9]+)$/);
if (nodesMatch) {
const token = nodesMatch[1];
const keys = loadKeys();
if (token !== keys.ZY_PROXY_SUB_TOKEN) {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: true, code: 'FORBIDDEN', message: '认证失败' }));
return;
}
const serverHost = getServerHost();
const nodes = buildNodeList(keys, serverHost);
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({
nodes,
total: nodes.length,
updated_at: new Date().toISOString()
}));
return;
}
// 服务状态端点: /status/{token}
// 返回服务器健康状态、节点列表、配额、守护状态
const statusMatch = pathname.match(/^\/status\/([a-f0-9]+)$/);
if (statusMatch) {
const token = statusMatch[1];
const keys = loadKeys();
if (token !== keys.ZY_PROXY_SUB_TOKEN) {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: true, code: 'FORBIDDEN', message: '认证失败' }));
return;
}
const serverHost = getServerHost();
const quota = getQuotaInfo();
const guardian = readGuardianStatus();
const nodes = buildNodeList(keys, serverHost);
const totalGB = quota.total_bytes / (1024 ** 3);
const usedGB = (quota.upload_bytes + quota.download_bytes) / (1024 ** 3);
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({
server: {
status: 'online',
version: '1.0.0',
uptime_seconds: Math.floor(process.uptime()),
region: 'sg',
region_name: 'Singapore'
},
nodes,
quota: {
total_gb: parseFloat(totalGB.toFixed(1)),
used_gb: parseFloat(usedGB.toFixed(1)),
remaining_gb: parseFloat((totalGB - usedGB).toFixed(1)),
percentage_used: parseFloat(((usedGB / totalGB) * 100).toFixed(1)),
period: quota.period,
reset_day: quota.reset_day,
upload_bytes: quota.upload_bytes,
download_bytes: quota.download_bytes
},
guardian: guardian ? {
status: guardian.status || 'unknown',
last_check: guardian.last_check,
consecutive_failures: guardian.consecutive_failures || 0,
auto_fixes: guardian.auto_fixes || 0,
last_issue: guardian.last_issue
} : { status: 'not_available' },
updated_at: new Date().toISOString()
}));
return;
}
// 测速端点信息: /speedtest/{token}
// 返回测速配置和端点
const speedtestMatch = pathname.match(/^\/speedtest\/([a-f0-9]+)$/);
if (speedtestMatch) {
const token = speedtestMatch[1];
const keys = loadKeys();
if (token !== keys.ZY_PROXY_SUB_TOKEN) {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: true, code: 'FORBIDDEN', message: '认证失败' }));
return;
}
const serverHost = getServerHost();
const cnRelayHost = getCnRelayHost();
const cnRelayPort = getCnRelayPort();
const endpoints = [
{
id: 'zy-sg-direct',
name: '🏛️ SG直连',
test_url: `http://${serverHost}/api/proxy-sub/health`,
ping_host: serverHost,
ping_port: 443
}
];
if (cnRelayHost) {
endpoints.push({
id: 'zy-cn-relay',
name: '🇨🇳 CN中转',
test_url: `http://${cnRelayHost}:${cnRelayPort}`,
ping_host: cnRelayHost,
ping_port: cnRelayPort
});
}
// 第三方测速URL (用于测试代理后的下载速度)
const download_test_urls = [
{ url: 'http://www.gstatic.com/generate_204', purpose: 'latency', size: 'tiny' },
{ url: 'https://speed.cloudflare.com/__down?bytes=1000000', purpose: 'download', size: '1MB' },
{ url: 'https://speed.cloudflare.com/__down?bytes=10000000', purpose: 'download', size: '10MB' }
];
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({
endpoints,
download_test_urls,
recommended_interval_seconds: 300,
updated_at: new Date().toISOString()
}));
return;
}
// 诊断端点: /diagnostics/{token}
// 返回诊断辅助信息、错误翻译表、检查项
const diagMatch = pathname.match(/^\/diagnostics\/([a-f0-9]+)$/);
if (diagMatch) {
const token = diagMatch[1];
const keys = loadKeys();
if (token !== keys.ZY_PROXY_SUB_TOKEN) {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: true, code: 'FORBIDDEN', message: '认证失败' }));
return;
}
const serverHost = getServerHost();
const guardian = readGuardianStatus();
// 客户端应执行的诊断检查列表
const client_checks = [
{
id: 'dns_resolution',
name: 'DNS解析检查',
description: '检查是否能解析服务器域名',
test_type: 'dns',
target: serverHost
},
{
id: 'tcp_connectivity',
name: 'TCP连接检查',
description: '检查443端口是否可达',
test_type: 'tcp',
target: serverHost,
port: 443,
timeout_ms: 5000
},
{
id: 'tls_handshake',
name: 'TLS握手检查',
description: '检查Reality TLS握手是否成功',
test_type: 'tls',
target: serverHost,
port: 443,
sni: 'www.microsoft.com',
timeout_ms: 10000
},
{
id: 'subscription_api',
name: '订阅服务检查',
description: '检查订阅API是否正常响应',
test_type: 'http',
url: `http://${serverHost}/api/proxy-sub/health`,
timeout_ms: 10000,
expected_status: 200
},
{
id: 'proxy_throughput',
name: '代理吞吐量检查',
description: '通过代理下载测试文件检测速度',
test_type: 'download',
url: 'https://speed.cloudflare.com/__down?bytes=1000000',
via_proxy: true,
timeout_ms: 30000
}
];
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({
error_translations: ERROR_TRANSLATIONS,
client_checks,
server_guardian: guardian ? {
status: guardian.status || 'unknown',
last_check: guardian.last_check,
last_issue: guardian.last_issue
} : { status: 'not_available' },
support: {
report_method: 'email',
admin_contact: 'zy-admin',
log_retention_hours: 24
},
client_version: {
minimum: '1.0.0',
recommended: '1.0.0',
config_format: 'sing-box-1.8+'
},
updated_at: new Date().toISOString()
}));
return;
}
// 404
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
} catch (err) {
console.error('❌ 请求处理错误 [%s %s]:', req.method, req.url, err.message || err);
try {
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
}
res.end('Internal Server Error');
} catch (writeErr) {
console.error(' ⚠️ 响应写入失败:', writeErr.message);
}
}
});
// 处理连接级别错误 (防止未捕获的socket错误导致进程崩溃)
server.on('error', (err) => {
console.error('❌ 服务器错误:', err.message);
if (err.code === 'EADDRINUSE') {
console.error(' 端口 %d 已被占用进程退出等待PM2重启...', PORT);
process.exit(1);
}
});
server.on('clientError', (err, socket) => {
console.error('⚠️ 客户端连接错误:', err.code || err.message);
if (socket.writable) {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
}
});
server.listen(PORT, '127.0.0.1', () => {
console.log(`🌐 铸渊专线订阅服务已启动: http://127.0.0.1:${PORT}`);
console.log(` 订阅端点: /sub/{token}`);
console.log(` 配额查询: /quota`);
console.log(` 健康检查: /health`);
console.log(` ── 铸渊客户端API ──`);
console.log(` 客户端配置: /client-config/{token} (sing-box JSON)`);
console.log(` 节点列表: /nodes/{token}`);
console.log(` 服务状态: /status/{token}`);
console.log(` 测速信息: /speedtest/{token}`);
console.log(` 诊断信息: /diagnostics/{token}`);
});
// Graceful shutdown
function gracefulShutdown(signal) {
console.log(`\n${signal} received. Shutting down gracefully...`);
const forceExit = setTimeout(() => { process.exit(1); }, 5000);
server.close(() => {
clearTimeout(forceExit);
console.log('Server closed.');
process.exit(0);
});
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
// 进程级错误保护 (记录后优雅退出由PM2负责重启)
process.on('uncaughtException', (err) => {
console.error('❌ 未捕获的异常:', err.message);
console.error(err.stack);
gracefulShutdown('uncaughtException');
});
process.on('unhandledRejection', (reason) => {
console.error('❌ 未处理的Promise拒绝:', reason);
});