netplaySniff – Blame information for rev 6
?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) => { |
||
61 | logger.info(`running as a daemon: ${options.database}`) |
||
62 | logger.info(`running as a daemon: ${options.config}`) |
||
1 | office | 63 | |
6 | office | 64 | // load configuration file. |
65 | var config = YAML.load(options.config) |
||
1 | office | 66 | |
6 | office | 67 | // Watch the configuration file for changes. |
68 | const configWatch = inotify.addWatch({ |
||
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 |
106 | const cap = new Cap() |
||
107 | const device = Cap.findDevice(`${config.router}`) |
||
108 | const filter = `tcp and dst port ${config.netplay.port} and dst host ${config.netplay.host}` |
||
109 | const bufSize = 10 * 1024 * 1024 |
||
110 | const buffer = Buffer.alloc(65535) |
||
111 | const linkType = cap.open(device, filter, bufSize, buffer) |
||
112 | cap.setMinBytes && cap.setMinBytes(0) |
||
113 | cap.on('packet', (bytes, truncated) => processPacket(bytes, truncated, config, mqttClient)) |
||
1 | office | 114 | |
6 | office | 115 | let netplay = {} |
116 | if (linkType !== 'ETHERNET') { |
||
117 | return |
||
118 | } |
||
1 | office | 119 | |
6 | office | 120 | var ret = decoders.Ethernet(buffer) |
1 | office | 121 | |
6 | office | 122 | if (ret.info.type !== PROTOCOL.ETHERNET.IPV4) { |
123 | return |
||
124 | } |
||
1 | office | 125 | |
6 | office | 126 | ret = decoders.IPV4(buffer, ret.offset) |
127 | netplay.ip = ret.info.srcaddr |
||
1 | office | 128 | |
6 | office | 129 | if (ret.info.protocol !== PROTOCOL.IP.TCP) { |
130 | return |
||
131 | } |
||
1 | office | 132 | |
6 | office | 133 | var dataLength = ret.info.totallen - ret.hdrlen |
1 | office | 134 | |
6 | office | 135 | ret = decoders.TCP(buffer, ret.offset) |
136 | dataLength -= ret.hdrlen |
||
1 | office | 137 | |
6 | office | 138 | var payload = buffer.subarray(ret.offset, ret.offset + dataLength) |
1 | office | 139 | |
6 | office | 140 | // look for the NETPLAY_CMD_NICK in "netplay_private.h" data marker. |
141 | if (payload.indexOf('0020', 0, "hex") !== 2) { |
||
2 | office | 142 | return |
143 | } |
||
6 | office | 144 | |
145 | // remove NULL and NETPLAY_CMD_NICK |
||
146 | netplay.nick = payload.toString().replace(/[\u0000\u0020]+/gi, '') |
||
147 | netplay.hash = shortHash(`${netplay.nick}${netplay.ip}`) |
||
148 | netplay.time = new Date().toISOString() |
||
149 | |||
150 | logger.info(`Player ${netplay.nick} joined via IP ${netplay.ip}`); |
||
151 | |||
152 | const db = new sqlite.Database(config.db.file, sqlite.OPEN_CREATE | sqlite.OPEN_READWRITE | sqlite.OPEN_FULLMUTEX, (error) => { |
||
153 | if (error) { |
||
154 | logger.error(`failed to open database: ${config.db.file}`) |
||
2 | office | 155 | return |
156 | } |
||
6 | office | 157 | |
158 | db.run(`CREATE TABLE IF NOT EXISTS "players" ("nick" TEXT(15) NOT NULL, "ip" TEXT NOT NULL)`, (error, result) => { |
||
159 | if (error) { |
||
160 | logger.error(`could not create database table: ${error}`); |
||
2 | office | 161 | return |
162 | } |
||
6 | office | 163 | db.run(`INSERT INTO "players" ("nick", "ip") VALUES ($nick, $ip)`, { $nick: netplay.nick, $ip: netplay.ip }, (error) => { |
164 | if (error) { |
||
165 | logger.error(`could not insert player and IP into database: ${error}`) |
||
166 | return |
||
167 | } |
||
168 | |||
169 | logger.info(`player added to database`) |
||
170 | }) |
||
2 | office | 171 | }) |
172 | }) |
||
173 | |||
6 | office | 174 | // send data to MQTT server |
175 | const data = JSON.stringify(netplay, null, 4) |
||
176 | mqttClient.publish(`${config.mqtt.topic}`, data, (error, packet) => { |
||
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}`) |
||
184 | exec(`iptables -t mangle -A PREROUTING -p tcp --src ${netplay.ip} --dport ${config.netplay.port} -j DROP`, (error, stdout, stderr) => { |
||
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 | } |
||
5 | office | 199 | }) |
2 | office | 200 | |
6 | office | 201 | program.parse() |