Coinhive挖矿脚本分析与Pool改造自建——这篇文章说浏览器挖矿,后端nodejs本质上就是代理!!!的确如此!!!
自己动手丰衣足食
本着一探究竟开源共享的精神,朝着拿回我的30%payback目标,我们已经详细分析了Coinhive挖矿脚本的构成、由来、运作方式,暂不提用户交互和兼容处理方式,我们先实现最核心的功能,构造一个属于自己的WebMoneroPool!
转化器思路构想
- 从现有开源的矿池项目直接二次构造,搭建兼容WebSocket通信方式的完整Pool。
优点:从矿池整体可控,全面覆盖各项设置,100%赚取算力价值,矿池直接收取价值。(一般Pool由中心矿池打款到矿工需满足至少有0.1XMR,其中自动“税收”扣去矿池运营捐赠0.5-2%,平台开发捐赠0.1%)
缺点:服务器配置需求较高(≥2C4G),对于小流量站点或者前端产品变现转换率较低,运营赤字风险大。其次矿池更新迭代需从原项目升级并重新修改,容易出现不必要的烦恼……
- 从头构造流量转换,用
Pool_Proxy
形式,对接转化WebSocket与PoolSocket,以中间件形式介入。优点:只需构造中间件,便于维护。可以单独形成Log,对搭建平台要求无过高要求。
缺点:无法从头操控,无法避免部分定量捐赠和Pool平台“税收”。
从优缺点看,我们首要选择从 Pool_Proxy
中间件方式来构造前端挖矿的服务端,而后端Pool的选择空间就更大了,可以选择现有的公开矿池,也可以另外结合再自己搭建矿池。
稍安勿躁,本文将分别讲解自建中间件的过程以及标准矿池的搭建方式。
中间件deepMiner构造
为了简化开发流程,我使用比较熟悉的nodejs举例实现(其他语言按需实现均可)。
中间件制作,用于转化WebSocket流量与PoolSocket(TCP)流量,给双方充当“翻译”角色。
所以基础框架,先获得两边的接口,并使其能够正常对接,所以我们需要先写入如下内容到一个新建的 server.js
里:
var http = require('http'), //web承载
WebSocket = require("ws"), //WebSocket实现
net = require('net'), //PoolSocket(TCP)实现
fs = require('fs'); //访问本地文件系统
我们先构造一个 config.json
文件用来设置构造所需的参数设定,比如域名地址,矿池地址,钱包地址,监听端口等:
{
"lhost": "127.0.0.1",
"lport": 7777,
"domain": "miner.deepwn.com",
"pool": "pool.usxmrpool.com:3333",
"addr": "41ynfGBUDbGJYYzz2jgS***************************************************",
"pass": ""
}
再继续往 server.js
里加入代码,读取配置文件并构造出大体框架。
先来一个web,确保外部访问正常:
var conf = fs.readFileSync(__dirname + '/config.json', 'utf8');
conf = JSON.parse(conf);
// Http web
var web = http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.end('Pool Worked!'); // Change at Next...
}).listen(conf.lport, conf.lhost);
// next codes here...
打开浏览器,从自己的127.0.0.1:7777已经能访问看到Pool Worked!
页面。
接下来完成复用构造WebSocket服务:
为了方便书写,我们先声明一个叫做 conn
的对象来接管所有设置内容,为了方便后期调用。
其中囊括了ws
服务,以及每次ws
新连接所触发的net.Socket()
,同时声明每个连接的 pid
来解决一个关于Miner_banned
的坑……
回顾一下:上篇稿件中提到的JsonRPC里,首次login验证中的id,其实是一个Miner的身份区分。
client >> { "method": "login", "params": { "login": "********** [ Wallet Addr ] **********", "pass": "", "agent": "xmr-stak-cpu/1.3.0-1.5.0" }, "id": 1 // <= 这个id 可以重复使用重复登录,但是过多为完成jobs,或者同时间id=1并发登录,将造成Miner被ban。 } server << { "id": 1, // <= 登录的MinerID,不用IP区分应该是担心存在DHCP下的Miner出口IP相同,能够理解…… "jsonrpc": "2.0", "error": null, "result": { "id": "811233385116793", "job": { "blob": "0606e498c5ce057326423f235dcd67dec07d9cb79e3506da8b35198e7debb40be3cbc2326c1999000000008bad7c9d5b78e9c9693903e817d20c09befe2c72ee6d20f297c0026d9a6e492406", "job_id": "664084446453489", "target": "711b0d00" }, "status": "OK" } }
这是pool用来区分单个IP不同miner的MinerID,如果一个IP里同一个Miner多次违约不完成Job并且重复登录申请新Job,将会进入banned模式,10分钟内无法获取新Jobs。
因为只是个demo,所以所有内容,全写在一个文件了,并没有进行区分和不同Socket线程单独控制,就全权交给http
来内部控制sessions开启和销毁吧!我们继续接着构造:
// Websocket 成功连接后,流程内部发出TCP_Socket连接Pool不单独控制TCP销毁
var srv = new WebSocket.Server({
server: web, // 这里从web接管ws的操作
path: "/proxy", //可以加上path区分
maxPayload: 256
});
srv.on('connection', (ws) => { //当连接成功时,我们开始构造conn
var conn = {
uid: null, //为后期框架管理面板区分不同站点的UID
pid: new Date().getTime(), //解决踩坑……区分MinerID
workerId: null, //来自PoolJobs内job_id
found: 0,
accepted: 0,
ws: ws, //this ws
pl: new net.Socket(), //TCP Socket
}
var pool = conf.pool.split(':');
conn.pl.connect(pool[1], pool[0]); //使用新conn.pl对象,TCPSocket介入Pool
// on.('event') & some func here...
});
我们可以 nc -lvvp 8888
本地监听,修改 config.json
里pool的地址为本地监听的接口,再通过浏览器构造WebSocket访问 ws://127.0.0.1:7777
来验证代码是否可以执行。
在一切顺利的情况下我们开始下一步,处理不同事件,现在可以将 // on.('event') & some func here...
改为:
// Trans func here...
conn.ws.on('message', (data) => {
ws2pool(data); // Trans WS2TCP
console.log('[>] Request: ' + conn.uid + '\n\n' + data + '\n');
});
conn.ws.on('error', (data) => {
console.log('[!] ' + conn.uid + ' WebSocket ' + data + '\n');
conn.pl.destroy();
});
conn.ws.on('close', () => {
console.log('[!] ' + conn.uid + ' offline.\n');
conn.pl.destroy();
});
conn.pl.on('data', (data) => {
pool2ws(data); // Trans TCP2WS
console.log('[<] Response: ' + conn.uid + '\n\n' + data + '\n');
});
conn.pl.on('error', (data) => {
console.log('[!] PoolSocket ' + data + '\n');
if (conn.ws.readyState !== 3) {
conn.ws.close();
}
});
conn.pl.on('close', () => {
console.log('[!] PoolSocket Closed.\n');
if (conn.ws.readyState !== 3) {
conn.ws.close();
}
});
-
conn.ws.on('event', [function])
接管了在不同情况下对WebSocket的处理方式。 -
conn.pl.on('event', [function])
接管了对接Pool的不同处理方式。
那么我们还少了什么? 对,如何转换Socket流量才是核心内容,我们替换刚才 // Trans func here...
为如下,开始勾画核心——Socket转换的Functions:
// Trans WebSocket to PoolSocket
function ws2pool(data) {
var buf;
data = JSON.parse(data);
switch (data.type) {
case 'auth':
{
conn.uid = data.params.site_key;
if (data.params.user) {
conn.uid += '@' + data.params.user;
}
buf = {
"method": "login",
"params": {
"login": conf.addr,
"pass": conf.pass,
"agent": "deepMiner"
},
"id": conn.pid
}
buf = JSON.stringify(buf) + '\n';
conn.pl.write(buf);
break;
}
case 'submit':
{
conn.found++;
buf = {
"method": "submit",
"params": {
"id": conn.workerId,
"job_id": data.params.job_id,
"nonce": data.params.nonce,
"result": data.params.result
},
"id": conn.pid
}
buf = JSON.stringify(buf) + '\n';
conn.pl.write(buf);
break;
}
}
}
// Trans PoolSocket to WebSocket
function pool2ws(data) {
var buf;
data = JSON.parse(data);
if (data.id === conn.pid && data.result) {
if (data.result.id) {
conn.workerId = data.result