UNPKG

shadowsocks-manager

Version:

A shadowsocks manager tool for multi user and traffic control.

414 lines (384 loc) 11.7 kB
const log4js = require('log4js'); const logger = log4js.getLogger('system'); const later = require('later'); later.date.localTime(); // const cron = appRequire('init/cron'); const dgram = require('dgram'); const client = dgram.createSocket('udp4'); const version = appRequire('package').version; const exec = require('child_process').exec; const https = require('https'); let clientIp = []; const config = appRequire('services/config').all(); const host = config.shadowsocks.address.split(':')[0]; const port = +config.shadowsocks.address.split(':')[1]; const mPort = +config.manager.address.split(':')[1]; client.bind(mPort); const knex = appRequire('init/knex').knex; let shadowsocksType = 'libev'; let isNewPython = false; let lastFlow; const sendPing = () => { sendMessage('ping'); sendMessage('list'); }; let existPort = []; let existPortUpdatedAt = Date.now(); const setExistPort = flow => { existPort = []; if(Array.isArray(flow)) { existPort = flow.map(f => +f.server_port); } else { for(const f in flow) { existPort.push(+f); } } existPortUpdatedAt = Date.now(); }; let firstFlow = true; let portsForLibev = []; const connect = () => { client.on('message', async msg => { const msgStr = new String(msg); if(msgStr.substr(0, 4) === 'pong') { shadowsocksType = 'python'; } else if(msgStr.substr(0, 2) === '[{') { isNewPython = true; portsForLibev = JSON.parse(msgStr); setExistPort(portsForLibev); } else if(msgStr.substr(0, 3) === '[\n\t') { shadowsocksType = 'libev'; portsForLibev = JSON.parse(msgStr); setExistPort(portsForLibev); } else if(msgStr.substr(0, 5) === 'stat:') { let flow = JSON.parse(msgStr.substr(5)); !isNewPython && setExistPort(flow); const realFlow = await compareWithLastFlow(flow, lastFlow); const getConnectedIp = port => { setTimeout(() => { getIp(+port).then(ips => { ips.forEach(ip => { clientIp.push({ port: +port, time: Date.now(), ip }); }); }); }, Math.ceil(Math.random() * 3 * 60 * 1000)); }; if((new Date()).getMinutes() % 3 === 0) { for(const rf in realFlow) { if(realFlow[rf]) { getConnectedIp(rf); } } } logger.info(`Receive flow from shadowsocks: (${ shadowsocksType })\n${JSON.stringify(realFlow, null, 2)}`); lastFlow = flow; const insertFlow = Object.keys(realFlow).map(m => { return { port: +m, flow: +realFlow[m], time: Date.now(), }; }).filter(f => { return f.flow > 0; }); const accounts = await knex('account').select(); if(shadowsocksType === 'python' && !isNewPython) { insertFlow.forEach(fe => { const account = accounts.filter(f => { return fe.port === f.port; })[0]; if(!account) { sendMessage(`remove: {"server_port": ${ fe.port }}`); } }); } else { portsForLibev.forEach(async f => { const account = accounts.filter(a => a.port === +f.server_port)[0]; if(!account) { await sendMessage(`remove: {"server_port": ${ f.server_port }}`); } else if (account.password !== f.password) { await sendMessage(`remove: {"server_port": ${ f.server_port }}`); await sendMessage(`add: {"server_port": ${ account.port }, "password": "${ account.password }"}`); } // else if (account.method && account.method !== f.method) { // await sendMessage(`remove: {"server_port": ${ f.server_port }}`); // await sendMessage(`add: {"server_port": ${ account.port }, "password": "${ account.password }"}`); // } }); } if(insertFlow.length > 0) { if(firstFlow) { firstFlow = false; } else { // const insertPromises = []; for(let i = 0; i < Math.ceil(insertFlow.length / 50); i++) { await knex('flow').insert(insertFlow.slice(i * 50, i * 50 + 50)); // insertPromises.push(insert); } // Promise.all(insertPromises).then(); } } }; }); client.on('error', err => { logger.error(`client error: `, err); }); client.on('close', () => { logger.error(`client close`); }); }; const sendMessage = message => { client.send(message, port, host); return Promise.resolve('ok'); }; const startUp = async () => { client.send(Buffer.from('ping'), port, host); if(config.runShadowsocks === 'python') { sendMessage(`remove: {"server_port": 65535}`); } const accounts = await knex('account').select([ 'port', 'password' ]); for(const account of accounts) { await sendMessage(`add: {"server_port": ${ account.port }, "password": "${ account.password }"}`); } }; const resend = async () => { if(Date.now() - existPortUpdatedAt >= 180 * 1000) { existPort = []; } const accounts = await knex('account').select([ 'port', 'password' ]); for(const account of accounts) { if(!existPort.includes(account.port)) { await sendMessage(`add: {"server_port": ${ account.port }, "password": "${ account.password }"}`); } } }; const restart = {}; const compareWithLastFlow = async (flow, lastFlow) => { if(shadowsocksType === 'python') { return flow; } const realFlow = {}; if(!lastFlow) { for(const f in flow) { if(flow[f] <= 768) { delete flow[f]; } } return flow; } for(const f in flow) { if(lastFlow[f]) { realFlow[f] = flow[f] - lastFlow[f]; if(realFlow[f] === 0 && flow[f] > 5 * 1000 * 1000 * 1000) { if(!restart[f]) { restart[f] = 1; } if(restart[f] < 30) { restart[f] += 1; continue; } const account = await knex('account').where({ port: +f }).then(s => s[0]); if(account) { await sendMessage(`remove: {"server_port": ${ account.port }}`); await sendMessage(`add: {"server_port": ${ account.port }, "password": "${ account.password }"}`); delete restart[f]; } } else { delete restart[f]; } } else { realFlow[f] = flow[f]; } } if(Object.keys(realFlow).map(m => realFlow[m]).sort((a, b) => a > b)[0] < 0) { return flow; } for(const r in realFlow) { if(realFlow[r] <= 768) { delete realFlow[r]; } } return realFlow; }; connect(); startUp(); later.setInterval(() => { resend(); sendPing(); getGfwStatus(); }, later.parse.text('every 1 mins')); // cron.minute(() => { // resend(); // sendPing(); // getGfwStatus(); // }, 1); const checkPortRange = (port) => { if(!config.shadowsocks.portRange) { return true; } const portRange = config.shadowsocks.portRange.split(','); let isInRange = false; portRange.forEach(f => { if(f.includes('-')) { const range = f.trim().split('-'); if(port >= +range[0] && port <= +range[1]) { isInRange = true; } } else if (port === +f) { isInRange = true; } }); return isInRange; }; const addAccount = async (port, password) => { try { if(!checkPortRange(port)) { return Promise.reject('error'); } await sendMessage(`add: {"server_port": ${ port }, "password": "${ password }"}`); await knex('account').insert({ port, password }); return { port, password }; } catch(err) { return Promise.reject('error'); } }; const removeAccount = async (port) => { try { const deleteAccount = await knex('account').where({ port, }).delete(); if(deleteAccount <= 0) { return Promise.reject('error'); } await knex('flow').where({ port, }).delete(); await sendMessage(`remove: {"server_port": ${ port }}`); return { port }; } catch(err) { return Promise.reject('error'); } }; const changePassword = async (port, password) => { try { const updateAccount = await knex('account').where({port}).update({ password, }); if(updateAccount <= 0) { return Promise.reject('error'); } await sendMessage(`remove: {"server_port": ${ port }}`); await sendMessage(`add: {"server_port": ${ port }, "password": "${ password }"}`); return { port, password }; } catch(err) { return Promise.reject('error'); } }; const listAccount = async () => { try { const accounts = await knex('account').select([ 'port', 'password' ]); return accounts; } catch(err) { return Promise.reject('error'); } }; const getFlow = async (options) => { try { const startTime = options.startTime || 0; const endTime = options.endTime || Date.now(); const accounts = await knex('account').select([ 'port' ]); const flows = await knex('flow').select([ 'port' ]) .sum('flow as sumFlow').groupBy('port') .whereBetween('time', [ startTime, endTime ]); accounts.map(m => { const flow = flows.filter(f => { return f.port === m.port; })[0]; if(flow) { m.sumFlow = flow.sumFlow; } else { m.sumFlow = 0; } return m; }); if(options.clear) { await knex('flow').whereBetween('time', [ startTime, endTime ]).delete(); } return accounts; } catch(err) { logger.error(err); return Promise.reject('error'); } }; let isGfw = 0; let getGfwStatusTime = null; const getGfwStatus = () => { if(getGfwStatusTime && isGfw === 0 && Date.now() - getGfwStatusTime < 600 * 1000) { return; } getGfwStatusTime = Date.now(); let site = 'baidu.com'; if(config.isGfwUrl) { site = config.isGfwUrl; } const req = https.request({ hostname: site.split(':')[0], port: +site.split(':')[1] || 443, path: '/', method: 'GET', timeout: 8000 + isGfw * 2000, }, res => { if(res.statusCode >= 200 && res.statusCode < 400) { isGfw = 0; } res.setEncoding('utf8'); res.on('data', (chunk) => {}); res.on('end', () => {}); }); req.on('timeout', () => { req.abort(); isGfw += 1; }); req.on('error', (e) => { isGfw += 1; }); req.end(); }; const getVersion = () => { return { version, isGfw: !!(isGfw > 5), }; }; const getIp = port => { let cmd = ''; let shell = ''; if (process.platform === 'win32') { cmd = `netstat -an | sls -Pattern ':${ port } ' | sls -Pattern 'ESTABLISHED' | %{$_.Line.Split(' ',[System.StringSplitOptions]::RemoveEmptyEntries)[2]} | %{$_.Split(':')[0]} | sls -Pattern '127\\.0\\.0\\.1' -NotMatch | unique | %{$_.Line}`; shell = 'powershell'; } else { cmd = `ss -an | grep ':${ port } ' | grep ESTAB | awk '{print $6}' | cut -d: -f1 | grep -v 127.0.0.1 | uniq -d`; shell = '/bin/sh'; } return new Promise((resolve, reject) => { exec(cmd, {shell: shell}, function(err, stdout, stderr){ if(err) { reject(stderr); } else { const result = []; stdout.split('\n').filter(f => f).forEach(f => { if(result.indexOf(f) < 0) { result.push(f); } }); resolve(result); } }); }); }; const getClientIp = port => { clientIp = clientIp.filter(f => { return Date.now() - f.time <= 15 * 60 * 1000; }); const result = []; clientIp.filter(f => { return Date.now() - f.time <= 15 * 60 * 1000 && f.port === port; }).map(m => { return m.ip; }).forEach(f => { if(result.indexOf(f) < 0) { result.push(f); } }); return result; }; exports.addAccount = addAccount; exports.removeAccount = removeAccount; exports.changePassword = changePassword; exports.listAccount = listAccount; exports.getFlow = getFlow; exports.getVersion = getVersion; exports.getClientIp = getClientIp;