netplaySniff – Blame information for rev 11

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