netplaySniff – Blame information for rev 1
?pathlinks?
Rev | Author | Line No. | Line |
---|---|---|---|
1 | office | 1 | #!/usr/bin/env node |
2 | /////////////////////////////////////////////////////////////////////////// |
||
3 | // Copyright (C) 2023 Wizardry and Steamworks - License: MIT // |
||
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() |
||
18 | |||
19 | // load configuration file. |
||
20 | let config = YAML.load('config.yml') |
||
21 | |||
22 | // set up logger. |
||
23 | const logger = createLogger({ |
||
24 | format: format.combine( |
||
25 | format.splat(), |
||
26 | format.simple() |
||
27 | ), |
||
28 | transports: [ |
||
29 | new transports.Console({ |
||
30 | timestamp: true |
||
31 | }), |
||
32 | new transports.File( |
||
33 | { |
||
34 | timestamp: true, |
||
35 | filename: path.join(path.dirname(fs.realpathSync(__filename)), "log/netplaySniff.log") |
||
36 | } |
||
37 | ) |
||
38 | ] |
||
39 | }) |
||
40 | |||
41 | // set up packet capture |
||
42 | const cap = new Cap() |
||
43 | const device = Cap.findDevice(`${config.router}`) |
||
44 | const filter = `tcp and dst port ${config.netplay.port} and dst host ${config.netplay.host}` |
||
45 | const bufSize = 10 * 1024 * 1024 |
||
46 | const buffer = Buffer.alloc(65535) |
||
47 | const linkType = cap.open(device, filter, bufSize, buffer) |
||
48 | |||
49 | cap.setMinBytes && cap.setMinBytes(0) |
||
50 | |||
51 | let nickBanSet = new Set(config.bans.nicknames) |
||
52 | |||
53 | // Watch the configuration file for changes. |
||
54 | const configWatch = inotify.addWatch({ |
||
55 | path: 'config.yml', |
||
56 | watch_for: Inotify.IN_MODIFY, |
||
57 | callback: function(event) { |
||
58 | logger.info(`Reloading configuration file config.yml`) |
||
59 | config = YAML.load('config.yml') |
||
60 | nickBanSet = new Set(config.bans.nicknames) |
||
61 | |||
62 | } |
||
63 | }) |
||
64 | |||
65 | const mqttClient = mqtt.connect(config.mqtt.connect) |
||
66 | |||
67 | mqttClient.on('reconnect', () => { |
||
68 | logger.info('Reconnecting to Corrade MQTT server...') |
||
69 | }) |
||
70 | |||
71 | mqttClient.on('connect', () => { |
||
72 | logger.info('Connected to Corrade MQTT server.') |
||
73 | // Subscribe to group message notifications with group name and password. |
||
74 | mqttClient.subscribe(`${config.mqtt.topic}`, (error) => { |
||
75 | if (error) { |
||
76 | logger.info('Error subscribing to Corrade MQTT group messages.') |
||
77 | return |
||
78 | } |
||
79 | |||
80 | logger.info('Subscribed to Corrade MQTT group messages.') |
||
81 | }) |
||
82 | }) |
||
83 | |||
84 | mqttClient.on('close', () => { |
||
85 | logger.error('Disconnected from Corrade MQTT server...') |
||
86 | }) |
||
87 | |||
88 | mqttClient.on('error', (error) => { |
||
89 | logger.error(`Error found while connecting to Corrade MQTT server ${error}`) |
||
90 | }) |
||
91 | |||
92 | cap.on('packet', function(bytes, truncated) { |
||
93 | let netplay = {} |
||
94 | |||
95 | if(linkType !== 'ETHERNET') { |
||
96 | return |
||
97 | } |
||
98 | |||
99 | var ret = decoders.Ethernet(buffer) |
||
100 | |||
101 | if (ret.info.type !== PROTOCOL.ETHERNET.IPV4) { |
||
102 | return |
||
103 | } |
||
104 | |||
105 | ret = decoders.IPV4(buffer, ret.offset) |
||
106 | netplay.ip = ret.info.srcaddr |
||
107 | |||
108 | if (ret.info.protocol !== PROTOCOL.IP.TCP) { |
||
109 | return |
||
110 | } |
||
111 | |||
112 | var dataLength = ret.info.totallen - ret.hdrlen |
||
113 | |||
114 | ret = decoders.TCP(buffer, ret.offset) |
||
115 | dataLength -= ret.hdrlen |
||
116 | |||
117 | var payload = buffer.subarray(ret.offset, ret.offset + dataLength) |
||
118 | |||
119 | // look for the NETPLAY_CMD_NICK in "netplay_private.h" data marker. |
||
120 | if(payload.indexOf('0020', 0, "hex") !== 2) { |
||
121 | return |
||
122 | } |
||
123 | |||
124 | // remove NULL and NETPLAY_CMD_NICK |
||
125 | netplay.nick = payload.toString().replace(/[\u0000\u0020]+/gi, '') |
||
126 | netplay.hash = shortHash(`${netplay.nick}${netplay.ip}`) |
||
127 | netplay.time = new Date().toISOString() |
||
128 | |||
129 | // ban by nick. |
||
130 | if(nickBanSet.has(netplay.nick)) { |
||
131 | logger.info(`nick found to be banned: ${netplay.nick}`) |
||
132 | exec(`/usr/sbin/iptables -t mangle -A PREROUTING -p tcp --src ${netplay.ip} --dport ${config.netplay.port} -j DROP`, (error, stdout, stderr) => { |
||
133 | if (error) { |
||
134 | logger.error(`Error returned while banning connecting client ${error.message}`) |
||
135 | return |
||
136 | } |
||
137 | if (stderr) { |
||
138 | logger.error(`Standard error returned ${stderr}`) |
||
139 | return |
||
140 | } |
||
141 | if(stdout) { |
||
142 | logger.info(`Standard error reported while banning ${typeof stdout}`) |
||
143 | return |
||
144 | } |
||
145 | |||
146 | // only send notification if the connecting nick isn't banned |
||
147 | const data = JSON.stringify(netplay, null, 4) |
||
148 | mqttClient.publish(`${config.mqtt.topic}`, data) |
||
149 | }) |
||
150 | } |
||
151 | |||
152 | }) |