netplaySniff – Blame information for rev 13

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 office 1 #!/usr/bin/env node
2 ///////////////////////////////////////////////////////////////////////////
2 office 3 // Copyright (C) 2024 Wizardry and Steamworks - License: MIT //
1 office 4 ///////////////////////////////////////////////////////////////////////////
5  
6 const fs = require('fs')
7 const path = require('path')
8 const { createLogger, format, transports } = require('winston')
9 const mqtt = require('mqtt')
10 const YAML = require('yamljs')
11 const Cap = require('cap').Cap
12 const decoders = require('cap').decoders
13 const PROTOCOL = decoders.PROTOCOL
12 office 14 const crypto = require('crypto')
1 office 15 const { exec } = require("child_process")
16 const Inotify = require('inotify-remastered').Inotify
17 const inotify = new Inotify()
5 office 18 const sqlite = require('sqlite3')
6 office 19 const { Command } = require('commander')
1 office 20  
6 office 21 // set up logger
1 office 22 const logger = createLogger({
23 format: format.combine(
2 office 24 format.timestamp({
25 format: 'YYYYMMDDHHmmss'
26 }),
6 office 27 format.printf(info =>
28 `${info.timestamp} ${info.level}: ${info.message}` + (info.splat !== undefined ? `${info.splat}` : " ")
2 office 29 )
1 office 30 ),
31 transports: [
32 new transports.Console({
33 timestamp: true
34 }),
35 new transports.File(
36 {
37 timestamp: true,
10 office 38 filename: path.join(path.dirname(fs.realpathSync(__filename)), 'log/netplaySniff.log')
1 office 39 }
40 )
41 ]
42 })
43  
6 office 44 const program = new Command()
45 program
46 .name('netplaySniff')
47 .description(`
48 +--+
49 | | Monitor netplay traffic, sniff users and their
50 | || | IP addresses and store them in a database.
51 +--+
52 `)
53 .version('1.0')
1 office 54  
6 office 55 program
56 .command('run')
57 .option('-c, --config <path>', 'path to the configuration file', 'config.yml')
58 .option('-d, --database <path>', 'the path where to store a database', 'db/players.db')
59 .description('run the program as a daemon')
60 .action((options) => {
10 office 61 logger.info(`running as a daemon with configuration from ${options.config} and database at ${options.database}`)
1 office 62  
10 office 63 // load configuration file.
64 var config = YAML.load(options.config)
1 office 65  
6 office 66 // Watch the configuration file for changes.
10 office 67 inotify.addWatch({
6 office 68 path: options.config,
69 watch_for: Inotify.IN_MODIFY,
70 callback: function (event) {
71 logger.info(`Reloading configuration file config.yml`)
72 config = YAML.load(options.config)
73 }
74 })
1 office 75  
6 office 76 const mqttClient = mqtt.connect(config.mqtt.connect)
1 office 77  
6 office 78 mqttClient.on('reconnect', () => {
79 logger.info('Reconnecting to MQTT server...')
80 })
1 office 81  
6 office 82 mqttClient.on('connect', () => {
83 logger.info('Connected to MQTT server.')
84 // Subscribe to group message notifications with group name and password.
85 mqttClient.subscribe(`${config.mqtt.topic}`, (error) => {
86 if (error) {
87 logger.info('Error subscribing to MQTT server.')
88 return
89 }
1 office 90  
6 office 91 logger.info('Subscribed to MQTT server.')
92 })
93 })
1 office 94  
6 office 95 mqttClient.on('close', () => {
96 logger.error('Disconnected from MQTT server.')
97 })
1 office 98  
6 office 99 mqttClient.on('error', (error) => {
100 logger.error(`MQTT ${error}`)
101 console.log(error)
102 })
1 office 103  
6 office 104 // set up packet capture
10 office 105 logger.info(`setting up packet capture`)
106  
6 office 107 const cap = new Cap()
10 office 108 const filter = `tcp and dst port ${config.netplay.port}`
6 office 109 const bufSize = 10 * 1024 * 1024
110 const buffer = Buffer.alloc(65535)
12 office 111 var device = Cap.findDevice(config.listen);
11 office 112 const linkType = cap.open(device, filter, bufSize, buffer)
6 office 113 cap.setMinBytes && cap.setMinBytes(0)
10 office 114 cap.on('packet', () => {
115 let netplay = {}
116 if (linkType !== 'ETHERNET') {
117 return
118 }
1 office 119  
10 office 120 var ret = decoders.Ethernet(buffer)
121 if (ret.info.type !== PROTOCOL.ETHERNET.IPV4) {
122 return
123 }
1 office 124  
10 office 125 ret = decoders.IPV4(buffer, ret.offset)
12 office 126 netplay.src = ret.info.srcaddr
127 netplay.dst = ret.info.dstaddr
10 office 128 if (ret.info.protocol !== PROTOCOL.IP.TCP) {
129 return
130 }
1 office 131  
10 office 132 var dataLength = ret.info.totallen - ret.hdrlen
1 office 133  
10 office 134 ret = decoders.TCP(buffer, ret.offset)
135 dataLength -= ret.hdrlen
1 office 136  
10 office 137 var payload = buffer.subarray(ret.offset, ret.offset + dataLength)
138 // look for the NETPLAY_CMD_NICK in "netplay_private.h" data marker.
139 if (payload.indexOf('0020', 0, "hex") !== 2) {
140 return
141 }
1 office 142  
10 office 143 // remove NULL and NETPLAY_CMD_NICK
144 netplay.nick = payload.toString().replace(/[\u0000\u0020]+/gi, '')
12 office 145 var shasum = crypto.createHash('sha1')
146 shasum.update(`${netplay.nick}${netplay.src}`)
147 netplay.hash = shasum.digest('hex')
10 office 148 netplay.time = new Date().toISOString()
1 office 149  
12 office 150 logger.info(`Player ${netplay.nick} joined via IP ${netplay.src}`);
1 office 151  
10 office 152 const db = new sqlite.Database(config.db.file, sqlite.OPEN_CREATE | sqlite.OPEN_READWRITE | sqlite.OPEN_FULLMUTEX, (error) => {
6 office 153 if (error) {
10 office 154 logger.error(`failed to open database: ${config.db.file}`)
2 office 155 return
156 }
10 office 157  
13 office 158 db.run(`CREATE TABLE IF NOT EXISTS "players" ("hash" TEXT(40) NOT NULL PRIMARY KEY, "nick" TEXT(15) NOT NULL, "ip" TEXT NOT NULL, "date" TEXT NOT NULL)`, (error, result) => {
6 office 159 if (error) {
10 office 160 logger.error(`could not create database table: ${error}`);
6 office 161 return
162 }
13 office 163 db.run(`REPLACE INTO "players" ("hash", "nick", "ip", "date") VALUES ($hash, $nick, $ip, DateTime('now'))`, { $hash: netplay.hash, $nick: netplay.nick, $ip: netplay.src }, (error) => {
10 office 164 if (error) {
165 logger.error(`could not insert player and IP into database: ${error}`)
166 return
167 }
6 office 168  
10 office 169 logger.info(`player added to database`)
170 })
6 office 171 })
2 office 172 })
173  
10 office 174 // send data to MQTT server
175 const data = JSON.stringify(netplay, null, 4)
12 office 176 mqttClient.publish(config.mqtt.topic, data, (error, packet) => {
10 office 177 logger.info(`player data sent to MQTT broker`)
178 })
179  
180 // ban by nick.
181 let nickBanSet = new Set(config.bans.nicknames)
182 if (nickBanSet.has(netplay.nick)) {
183 logger.info(`nick found to be banned: ${netplay.nick}`)
13 office 184 exec(`/usr/sbin/iptables -t mangle -A PREROUTING -p tcp --src ${netplay.src} --dport ${config.netplay.port} -j DROP`, (error, stdout, stderr) => {
10 office 185 if (error) {
186 logger.error(`Error returned while banning connecting client ${error.message}`)
187 return
188 }
189 if (stderr) {
190 logger.error(`Standard error returned ${stderr}`)
191 return
192 }
193 if (stdout) {
194 logger.info(`Standard error reported while banning ${typeof stdout}`)
195 return
196 }
197 })
198 }
6 office 199 })
5 office 200 })
2 office 201  
6 office 202 program.parse()