netplaySniff – Blame information for rev 9
?pathlinks?
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') |
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, |
||
38 | filename: path.join(path.dirname(fs.realpathSync(__filename)), "log/netplaySniff.log") |
||
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) => { |
||
7 | office | 61 | logger.info(`running as a daemon with configuraton options from ${options.config} and database at ${options.database}`) |
1 | office | 62 | |
63 | |||
6 | office | 64 | // Watch the configuration file for changes. |
65 | const configWatch = inotify.addWatch({ |
||
66 | path: options.config, |
||
67 | watch_for: Inotify.IN_MODIFY, |
||
68 | callback: function (event) { |
||
69 | logger.info(`Reloading configuration file config.yml`) |
||
70 | config = YAML.load(options.config) |
||
7 | office | 71 | config.db.file = options.database |
6 | office | 72 | } |
73 | }) |
||
1 | office | 74 | |
7 | office | 75 | // load configuration file. |
76 | var config = YAML.load(options.config) |
||
77 | // override configuration options with command-line options |
||
78 | config.db.file = options.database |
||
79 | |||
6 | office | 80 | const mqttClient = mqtt.connect(config.mqtt.connect) |
1 | office | 81 | |
6 | office | 82 | mqttClient.on('reconnect', () => { |
83 | logger.info('Reconnecting to MQTT server...') |
||
84 | }) |
||
1 | office | 85 | |
6 | office | 86 | mqttClient.on('connect', () => { |
87 | logger.info('Connected to MQTT server.') |
||
88 | // Subscribe to group message notifications with group name and password. |
||
89 | mqttClient.subscribe(`${config.mqtt.topic}`, (error) => { |
||
90 | if (error) { |
||
91 | logger.info('Error subscribing to MQTT server.') |
||
92 | return |
||
93 | } |
||
1 | office | 94 | |
6 | office | 95 | logger.info('Subscribed to MQTT server.') |
96 | }) |
||
97 | }) |
||
1 | office | 98 | |
6 | office | 99 | mqttClient.on('close', () => { |
100 | logger.error('Disconnected from MQTT server.') |
||
101 | }) |
||
1 | office | 102 | |
6 | office | 103 | mqttClient.on('error', (error) => { |
104 | logger.error(`MQTT ${error}`) |
||
105 | console.log(error) |
||
106 | }) |
||
1 | office | 107 | |
6 | office | 108 | // set up packet capture |
109 | const cap = new Cap() |
||
8 | office | 110 | let device = {} |
111 | switch (config.router) { |
||
112 | case 'any': |
||
9 | office | 113 | device = config.router |
8 | office | 114 | break; |
115 | default: |
||
116 | device = Cap.findDevice(`${config.router}`) |
||
117 | break; |
||
118 | } |
||
6 | office | 119 | const filter = `tcp and dst port ${config.netplay.port} and dst host ${config.netplay.host}` |
120 | const bufSize = 10 * 1024 * 1024 |
||
121 | const buffer = Buffer.alloc(65535) |
||
122 | const linkType = cap.open(device, filter, bufSize, buffer) |
||
123 | cap.setMinBytes && cap.setMinBytes(0) |
||
124 | cap.on('packet', (bytes, truncated) => processPacket(bytes, truncated, config, mqttClient)) |
||
1 | office | 125 | |
6 | office | 126 | let netplay = {} |
127 | if (linkType !== 'ETHERNET') { |
||
128 | return |
||
129 | } |
||
1 | office | 130 | |
6 | office | 131 | var ret = decoders.Ethernet(buffer) |
1 | office | 132 | |
6 | office | 133 | if (ret.info.type !== PROTOCOL.ETHERNET.IPV4) { |
134 | return |
||
135 | } |
||
1 | office | 136 | |
6 | office | 137 | ret = decoders.IPV4(buffer, ret.offset) |
138 | netplay.ip = ret.info.srcaddr |
||
1 | office | 139 | |
6 | office | 140 | if (ret.info.protocol !== PROTOCOL.IP.TCP) { |
141 | return |
||
142 | } |
||
1 | office | 143 | |
6 | office | 144 | var dataLength = ret.info.totallen - ret.hdrlen |
1 | office | 145 | |
6 | office | 146 | ret = decoders.TCP(buffer, ret.offset) |
147 | dataLength -= ret.hdrlen |
||
1 | office | 148 | |
6 | office | 149 | var payload = buffer.subarray(ret.offset, ret.offset + dataLength) |
1 | office | 150 | |
6 | office | 151 | // look for the NETPLAY_CMD_NICK in "netplay_private.h" data marker. |
152 | if (payload.indexOf('0020', 0, "hex") !== 2) { |
||
2 | office | 153 | return |
154 | } |
||
6 | office | 155 | |
156 | // remove NULL and NETPLAY_CMD_NICK |
||
157 | netplay.nick = payload.toString().replace(/[\u0000\u0020]+/gi, '') |
||
158 | netplay.hash = shortHash(`${netplay.nick}${netplay.ip}`) |
||
159 | netplay.time = new Date().toISOString() |
||
160 | |||
161 | logger.info(`Player ${netplay.nick} joined via IP ${netplay.ip}`); |
||
162 | |||
163 | const db = new sqlite.Database(config.db.file, sqlite.OPEN_CREATE | sqlite.OPEN_READWRITE | sqlite.OPEN_FULLMUTEX, (error) => { |
||
164 | if (error) { |
||
7 | office | 165 | logger.error(`failed to open database: ${error}`) |
2 | office | 166 | return |
167 | } |
||
6 | office | 168 | |
169 | db.run(`CREATE TABLE IF NOT EXISTS "players" ("nick" TEXT(15) NOT NULL, "ip" TEXT NOT NULL)`, (error, result) => { |
||
170 | if (error) { |
||
171 | logger.error(`could not create database table: ${error}`); |
||
2 | office | 172 | return |
173 | } |
||
6 | office | 174 | db.run(`INSERT INTO "players" ("nick", "ip") VALUES ($nick, $ip)`, { $nick: netplay.nick, $ip: netplay.ip }, (error) => { |
175 | if (error) { |
||
176 | logger.error(`could not insert player and IP into database: ${error}`) |
||
177 | return |
||
178 | } |
||
179 | |||
180 | logger.info(`player added to database`) |
||
181 | }) |
||
2 | office | 182 | }) |
183 | }) |
||
184 | |||
6 | office | 185 | // send data to MQTT server |
186 | const data = JSON.stringify(netplay, null, 4) |
||
187 | mqttClient.publish(`${config.mqtt.topic}`, data, (error, packet) => { |
||
188 | logger.info(`player data sent to MQTT broker`) |
||
189 | }) |
||
190 | |||
191 | // ban by nick. |
||
192 | let nickBanSet = new Set(config.bans.nicknames) |
||
193 | if (nickBanSet.has(netplay.nick)) { |
||
194 | logger.info(`nick found to be banned: ${netplay.nick}`) |
||
195 | exec(`iptables -t mangle -A PREROUTING -p tcp --src ${netplay.ip} --dport ${config.netplay.port} -j DROP`, (error, stdout, stderr) => { |
||
196 | if (error) { |
||
197 | logger.error(`Error returned while banning connecting client ${error.message}`) |
||
198 | return |
||
199 | } |
||
200 | if (stderr) { |
||
201 | logger.error(`Standard error returned ${stderr}`) |
||
202 | return |
||
203 | } |
||
204 | if (stdout) { |
||
205 | logger.info(`Standard error reported while banning ${typeof stdout}`) |
||
206 | return |
||
207 | } |
||
208 | }) |
||
209 | } |
||
5 | office | 210 | }) |
2 | office | 211 | |
6 | office | 212 | program.parse() |