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