netplaySniff – Blame information for rev 3

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()
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 })