#!/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); });