// 铸渊专线订阅服务 · 流量计数 + subscription-userinfo 头 // 部署:BS-GZ-006 · 2026-08-23 · 配额 1000GiB/自然月重置 // 计数源:iptables 计数规则(relay 端口 8443-8446 进出字节) const http = require('http'); const fs = require('fs'); const path = require('path'); const { execFile } = require('child_process'); const PORT = 3944; const DATA_DIR = '/var/www/guanghulab-front-door/zy-sub'; const STATE_FILE = '/opt/zy-sub-service/state.json'; const TOTAL_QUOTA = 1000 * 1024 * 1024 * 1024; let state = { month: '', upload: 0, download: 0, lastIn: 0, lastOut: 0 }; try { Object.assign(state, JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'))); } catch {} function monthKey(d = new Date()) { return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0'); } function save() { try { fs.writeFileSync(STATE_FILE, JSON.stringify(state)); } catch (e) { console.error('save fail:', e.message); } } function readCounter(chain, tag, cb) { execFile('iptables-save', ['-c'], (err, out) => { let bytes = 0; if (!err) { for (const line of out.split('\n')) { if (line.includes('zy-sub-acct-' + tag) && line.includes('-A ' + chain + ' ')) { const m = line.match(/^\[(\d+):(\d+)\]/); if (m) bytes += parseInt(m[2], 10); } } } cb(bytes); }); } function poll() { readCounter('INPUT', 'in', (inB) => { readCounter('OUTPUT', 'out', (outB) => { const mk = monthKey(); if (state.month !== mk) { state.month = mk; state.upload = 0; state.download = 0; } if (inB < state.lastIn) state.lastIn = inB; // 计数器重启归零时对齐 if (outB < state.lastOut) state.lastOut = outB; state.upload += inB - state.lastIn; state.download += outB - state.lastOut; state.lastIn = inB; state.lastOut = outB; save(); }); }); } setInterval(poll, 60 * 1000); poll(); const used = () => state.upload + state.download; const userinfo = () => `upload=${state.upload}; download=${state.download}; total=${TOTAL_QUOTA}; expire=0`; http.createServer((req, res) => { const url = req.url.split('?')[0]; const m = url.match(/^\/zy-sub\/([0-9a-f]{16,64})\/(clash\.yaml|shadowrocket\.txt|status)$/); if (!m) { res.writeHead(404); return res.end('not found'); } if (m[2] === 'status') { res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ month: state.month, upload: state.upload, download: state.download, total: TOTAL_QUOTA, used: used(), remaining: Math.max(0, TOTAL_QUOTA - used()) }, null, 2)); } fs.readFile(path.join(DATA_DIR, m[1], m[2]), (err, data) => { if (err) { res.writeHead(404); return res.end('not found'); } res.writeHead(200, { 'Content-Type': m[2].endsWith('.yaml') ? 'text/yaml; charset=utf-8' : 'text/plain; charset=utf-8', 'subscription-userinfo': userinfo(), 'profile-update-interval': '24' }); res.end(data); }); }).listen(PORT, '127.0.0.1', () => console.log('zy-sub-service on', PORT));