31 Commits

Author SHA1 Message Date
16388c73e5 Added additional check for packetStream 2021-11-29 15:11:14 +01:00
8211f55b89 Added check for tcpdump-process stdout 2021-11-29 15:10:42 +01:00
c28bbaaada Moved Stream-creations outside 2021-11-29 15:09:59 +01:00
4ddbe3f06f Implemented userHelper methods to detect if streams have data before timeout 2021-11-29 15:09:21 +01:00
0d84079ce1 Added packetType to tag-fields 2021-11-29 14:20:29 +01:00
1bf761970f Added error-handling for regexBlockStream 2021-11-29 13:45:36 +01:00
5a0118aedd Added error-handling for process 2021-11-29 13:45:15 +01:00
0709db0ddf Changed exit-log to debug 2021-11-29 13:44:37 +01:00
c27761322c Catch start-error from tcpdump
tcpdump writes to stderr at the beginning
2021-11-29 13:18:29 +01:00
45a11753de Add own logger for tcpdump 2021-11-29 13:16:23 +01:00
d482001cdc Merge branch 'f_docker' into dev 2021-11-29 13:12:00 +01:00
a681bbd2d2 Removed sudo as container runs as root anyways 2021-11-29 13:11:11 +01:00
d9ee804c3b Removed src-references as hats the workdir 2021-11-29 13:10:44 +01:00
e320d8670b Add installation of package tcpdump 2021-11-29 13:10:05 +01:00
2d824543d1 fixed cmd 2021-11-29 13:09:49 +01:00
6e080907d1 Merge branch 'code-smell_handling' into dev 2021-11-29 09:58:04 +01:00
91c3aca9e2 Add function to get handshakeStage from keyInfo 2021-11-29 09:57:33 +01:00
31ab10c3e1 Fix wrong var-name 2021-11-29 09:57:05 +01:00
8ff7211f0f Revert "Extract packetType handling in own function"
This reverts commit 227ba127f8.
2021-11-29 09:50:14 +01:00
2ae85ababb Extract keyInfo reading to helper-file 2021-11-29 09:37:28 +01:00
227ba127f8 Extract packetType handling in own function 2021-11-29 09:36:21 +01:00
8417de5756 Extract checks with if-statements 2021-11-29 09:35:17 +01:00
54eadf3bc3 Fixed Regex codesmells 2021-11-29 09:24:18 +01:00
e39ebaac23 Fixed Regex codesmell 2021-11-29 09:23:08 +01:00
54d627d469 Fix typo 2021-11-29 09:20:38 +01:00
ca3c37be0f Activating strict-mode 2021-11-29 09:16:49 +01:00
96b52e63a0 Add exit-handler with error-detection 2021-11-29 09:16:39 +01:00
3c5e941cba Creating & Attaching streams / Error-logger 2021-11-29 09:15:49 +01:00
a468d7a57b Start tcpdump process 2021-11-29 09:14:50 +01:00
4ad5eba7e0 Change tag-srtting to recursive by field
This will properly set e.g. arrays
2021-11-26 21:03:11 +01:00
2646c9787e Changed key-checking to value-checking
This includes e.g. gettings, originally not included in keys
2021-11-26 21:00:15 +01:00
8 changed files with 201 additions and 33 deletions

View File

@@ -7,7 +7,11 @@ WORKDIR /usr/src/app
COPY package*.json ./ COPY package*.json ./
RUN npm install RUN npm install
RUN apt-get update
RUN apt-get -y install \
tcpdump
# Bundle app source # Bundle app source
COPY ./src/ . COPY ./src/ .
CMD ["npm", "run"] CMD ["npm", "run", "start"]

View File

@@ -2,10 +2,10 @@
"name": "rfmon-to-influx", "name": "rfmon-to-influx",
"version": "1.0.0", "version": "1.0.0",
"description": "Writing (mostly meta-) data received in Wireless-Monitor-Mode into an InfluxDB", "description": "Writing (mostly meta-) data received in Wireless-Monitor-Mode into an InfluxDB",
"main": "src/main.js", "main": "main.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"start": "node src/main.js" "start": "node main.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

46
src/helper/userHelper.js Normal file
View File

@@ -0,0 +1,46 @@
// This file specifies functions to help a user with e.g. configuration-errors
function detectStreamData(stream, timeout = 5000){
return new Promise((resolve, reject) => {
let timeoutHandler;
if(timeout){
timeoutHandler = setTimeout(() => {
reject('timeout');
remListeners();
},
timeout);
}
function remListeners(){
stream.removeListener('error', errorHandler);
stream.removeListener('data', dataHandler);
if(timeoutHandler) clearTimeout(timeoutHandler);
}
function errorHandler(err) {
remListeners();
}
function dataHandler(data) {
resolve(data);
remListeners();
}
stream.on('error', errorHandler);
stream.on('data', dataHandler);
});
}
function detectStreamsData(streams, timeout = 5000){
let promises = [];
streams.forEach((stream) => {
promises.push(detectStreamData(stream, timeout));
})
return promises;
}
// Specify exports
module.exports = {
detectStreamData,
detectStreamsData,
};

View File

@@ -0,0 +1,43 @@
const { HandshakeStage } = require.main.require('./dto/Packet.js');
function keyInfoFromRaw(keyInfoRaw) {
return {
"KeyDescriptorVersion": keyInfoRaw>>0 & 0b111,
"KeyType": keyInfoRaw>>3 & 0b1,
"KeyIndex": keyInfoRaw>>4 & 0b11,
"Install": keyInfoRaw>>6 & 0b1,
"KeyACK": keyInfoRaw>>7 & 0b1,
"KeyMIC": keyInfoRaw>>8 & 0b1,
"Secure": keyInfoRaw>>9 & 0b1,
"Error": keyInfoRaw>>10 & 0b1,
"Request": keyInfoRaw>>11 & 0b1,
"EncryptedKeyData": keyInfoRaw>>12 & 0b1,
"SMKMessage": keyInfoRaw>>13 & 0b1,
};
}
const HANDSHAKE_STAGE_KEYINFO = {
"keys": ["Install", "KeyACK", "KeyMIC", "Secure"],
"0100": HandshakeStage[1],
"0010": HandshakeStage[2],
"1111": HandshakeStage[3],
"0011": HandshakeStage[4],
};
function handshakeStageFromKeyInfo(keyInfo){
// Extract compare-keys
let keyData = "";
for (const key of HANDSHAKE_STAGE_KEYINFO['keys']) {
keyData += keyInfo[key].toString();
}
// Get and return stage
return HANDSHAKE_STAGE_KEYINFO[keyData];
}
// Specify exports
module.exports = {
keyInfoFromRaw,
handshakeStageFromKeyInfo,
};

View File

@@ -1,10 +1,22 @@
const logger = require("./helper/logger.js")("main"); "use strict";
const logFactory = require("./helper/logger.js");
const logger = logFactory("main");
const { requireEnvVars } = require("./helper/env.js"); const { requireEnvVars } = require("./helper/env.js");
const { exit } = require("process"); const { exit } = require("process");
const { exec } = require("./helper/exec.js");
const { InfluxDB } = require('@influxdata/influxdb-client'); const { InfluxDB } = require('@influxdata/influxdb-client');
const InfluxChecks = require('./helper/influx-checks.js'); const InfluxChecks = require('./helper/influx-checks.js');
const { RegexBlockStream } = require("./streamHandler/RegexBlockStream.js");
const { PacketStreamFactory } = require("./streamHandler/PacketStreamFactory.js");
const { PacketInfluxPointFactory } = require("./streamHandler/PacketInfluxPointFactory.js");
const { InfluxPointWriter } = require("./streamHandler/InfluxPointWriter.js");
const userHelper = require("./helper/userHelper.js");
/// Setup ENVs /// Setup ENVs
const env = process.env; const env = process.env;
// Defaults // Defaults
@@ -46,4 +58,64 @@ if(errorMsg){
logger.info("Influx ok"); logger.info("Influx ok");
logger.info("Starting tcpdump..");
const TCPDUMP_BASECMD = "tcpdump -vvv -e -n -X -s0 -i"
let cmd = `${TCPDUMP_BASECMD} ${env.WIFI_INTERFACE}`;
let proc = exec(cmd);
logger.debug("Creating & Attaching streams..");
let regexBlockStream = new RegexBlockStream(/^\d{2}:\d{2}:\d{2}.\d{6}.*(\n( {4,8}|\t\t?).*)+\n/gm);
let packetStreamFactory = new PacketStreamFactory();
let packetInfluxPointFactory = new PacketInfluxPointFactory();
let influxPointWriter = new InfluxPointWriter(influxDb, env.INFLUX_ORG, env.INFLUX_BUCKET);
proc.stdout
.setEncoding("utf8")
.pipe(regexBlockStream)
.pipe(packetStreamFactory)
.pipe(packetInfluxPointFactory)
.pipe(influxPointWriter);
logger.debug("Attaching error-logger..");
const loggerTcpdump = logFactory("tcpdump");
proc.stderr.setEncoding("utf8").on("data", (data) => {
if(!data.match(/^(tcpdump: )?listening on /i)) // Catch start-error
loggerTcpdump.error(data);
});
regexBlockStream.on('error', (err) => {
if(err) loggerTcpdump.error(err);
})
proc.on("error", (err) => {
loggerTcpdump.error(err);
});
const loggerPacketStream = logFactory("PacketStreamFactory");
userHelper.detectStreamData(proc.stdout, 10000) // Expect tcpdump-logs to have data after max. 10s
.then(() => {
loggerTcpdump.debug("Got first data");
userHelper.detectStreamData(packetStreamFactory, 10000) // Expect then to have packets after further 10s
.then(() => {
loggerPacketStream.debug("Got first packet");
})
.catch((err) => {
if(err == 'timeout') loggerPacketStream.warn("No packets");
});
})
.catch((err) => {
if(err == 'timeout') loggerTcpdump.warn("No data after 10s! Wrong configuration?");
});
logger.debug("Attaching exit-handler..");
proc.on("exit", (code) => {
loggerTcpdump.debug(`tcpdump exited code: ${code}`);
if (code) {
loggerTcpdump.fatal(`tcpdump exited with non-zero code: ${code}`);
exit(1);
}
logger.info("Shutdown");
exit(0);
});
logger.info("Startup complete");
})(); })();

View File

@@ -9,6 +9,7 @@ const TAG_LIST = [
"bssid", "bssid",
"frequency", "frequency",
"flags", "flags",
"packetType",
]; ];
/** Measurement-name and corresponding field-key */ /** Measurement-name and corresponding field-key */
@@ -37,13 +38,16 @@ class PacketInfluxPointFactory extends Transform{
_transform(packet, encoding, next){ _transform(packet, encoding, next){
// Create measurements // Create measurements
MEASUREMENT_MAP.forEach((objKey, measurement) => { MEASUREMENT_MAP.forEach((objKey, measurement) => {
if(!Object.keys(packet).includes(objKey)) return; if(packet[objKey] == null) return;
let point = new Point(measurement); // Create point let point = new Point(measurement); // Create point
// Set tags // Set tags
TAG_LIST.filter(tag => Object.keys(packet).includes(tag)) TAG_LIST.filter(tag => Object.keys(packet).includes(tag)) // Filter tags available on object
.forEach(tag => point.tag(tag, packet[tag])); .filter(tag => packet[tag] != null) // Filter tags not falsy on object
.forEach(tag => {
tagObjectRecursively(point, tag, packet[tag]);
});
point.setField('value', packet[objKey]); // Set field point.setField('value', packet[objKey]); // Set field
@@ -54,6 +58,15 @@ class PacketInfluxPointFactory extends Transform{
} }
} }
function tagObjectRecursively(point, tag, field, suffix = ""){
if(typeof(field) == "object"){
// TODO: Convert boolean-arrays like "packet.flags" to key: value
Object.entries(field).map(([key, value]) => {
tagObjectRecursively(point, tag, value, `_${key}${suffix}`);
});
}
else point.tag(tag+suffix, field);
}
/** Mapping for type -> field-method */ /** Mapping for type -> field-method */
const POINT_FIELD_TYPE = new Map([ const POINT_FIELD_TYPE = new Map([

View File

@@ -3,6 +3,7 @@ const { Transform } = require('stream');
const { DateTime } = require("luxon"); const { DateTime } = require("luxon");
const { PacketType, FlagType, Packet, PacketWithSSID, BeaconPacket, ProbeRequestPacket, ProbeResponsePacket, AuthenticationPacket, AuthenticationType, AssociationResponsePacket, DisassociationPacket, HandshakePacket, HandshakeStage } = require.main.require('./dto/Packet.js'); const { PacketType, FlagType, Packet, PacketWithSSID, BeaconPacket, ProbeRequestPacket, ProbeResponsePacket, AuthenticationPacket, AuthenticationType, AssociationResponsePacket, DisassociationPacket, HandshakePacket, HandshakeStage } = require.main.require('./dto/Packet.js');
const hexConv = require.main.require("./helper/hexConverter.js"); const hexConv = require.main.require("./helper/hexConverter.js");
const wifiStateAnalyser = require.main.require("./helper/wifiStateAnalyzer.js");
const PACKET_TYPE_MAP = { const PACKET_TYPE_MAP = {
"Beacon": PacketType.Beacon, "Beacon": PacketType.Beacon,
@@ -65,17 +66,22 @@ class PacketStreamFactory extends Transform{
?.forEach(match => packet.flags[FLAG_TYPE_MAP[match]] = true) // Set them to true in flags ?.forEach(match => packet.flags[FLAG_TYPE_MAP[match]] = true) // Set them to true in flags
); );
packet.dataRate = Number(data.match(/(?<=^|\s)[0-9]+(\.[0-9]+)?(?=\sMb\/?s($|\s))/i)?.[0]) || null; packet.dataRate = Number(data.match(/(?<=^|\s)\d+(\.\d+)?(?=\sMb\/?s($|\s))/i)?.[0]) || null;
packet.frequency = Number(data.match(/(?<=^|\s)[0-9]{4}(?=\sMHz($|\s))/i)?.[0]) || null; packet.frequency = Number(data.match(/(?<=^|\s)\d{4}(?=\sMHz($|\s))/i)?.[0]) || null;
packet.durationMicros = Number(data.match(/(?<=^|\s)[0-9]{1,4}(?=us($|\s))/i)?.[0]) || null; packet.durationMicros = Number(data.match(/(?<=^|\s)\d{1,4}(?=us($|\s))/i)?.[0]) || null;
packet.signal = Number(data.match(/(?<=^|\s)-[0-9]{2,3}(?=dBm\sSignal($|\s))/i)?.[0]) || null; packet.signal = Number(data.match(/(?<=^|\s)-\d{2,3}(?=dBm\sSignal($|\s))/i)?.[0]) || null;
let packetTypeStr = data.match(new RegExp('(?<=^|\\s)('+ PACKET_TYPES_REGEX +')(?=$|\\s)', 'i'))?.[0]; let packetTypeStr = data.match(new RegExp('(?<=^|\\s)('+ PACKET_TYPES_REGEX +')(?=$|\\s)', 'i'))?.[0];
packet.packetType = packetTypeStr? PACKET_TYPE_MAP[packetTypeStr]: if(packetTypeStr)
data.match(/(SA|TA|DA|RA|BSSID):.{17}\s*$/i)? PacketType.NoData: packet.packetType = PACKET_TYPE_MAP[packetTypeStr];
PacketType.Unknown; else if(data.match(/(SA|TA|DA|RA|BSSID):.{17}\s*$/i)){
packet.packetType = PacketType.NoData
}
else {
packet.packetType = PacketType.Unknown;
}
packet.srcMac = data.match(/(?<=(^|\s)(SA|TA):).{17}(?=$|\s)/i)?.[0] ?? null; packet.srcMac = data.match(/(?<=(^|\s)(SA|TA):).{17}(?=$|\s)/i)?.[0] ?? null;
@@ -129,25 +135,9 @@ class PacketStreamFactory extends Transform{
// Read key-information // Read key-information
const keyInfoRaw = (packet.payloadData[0x5]<<0x8) + packet.payloadData[0x6]; const keyInfoRaw = (packet.payloadData[0x5]<<0x8) + packet.payloadData[0x6];
const keyInfo = { const keyInfo = wifiStateAnalyser.keyInfoFromRaw(keyInfoRaw); // Convert
"KeyDescriptorVersion": keyInfoRaw>>0 & 0b111,
"KeyType": keyInfoRaw>>3 & 0b1,
"KeyIndex": keyInfoRaw>>4 & 0b11,
"Install": keyInfoRaw>>6 & 0b1,
"KeyACK": keyInfoRaw>>7 & 0b1,
"KeyMIC": keyInfoRaw>>8 & 0b1,
"Secure": keyInfoRaw>>9 & 0b1,
"Error": keyInfoRaw>>10 & 0b1,
"Request": keyInfoRaw>>11 & 0b1,
"EncryptedKeyData": keyInfoRaw>>12 & 0b1,
"SMKMessage": keyInfoRaw>>13 & 0b1,
};
newPacket.handshakeStage = (!keyInfo.Install && keyInfo.KeyACK && !keyInfo.KeyMIC && !keyInfo.Secure)? HandshakeStage[1] : newPacket.handshakeStage = wifiStateAnalyser.handshakeStageFromKeyInfo(keyInfo); // Get stage
(!keyInfo.Install && !keyInfo.KeyACK && keyInfo.KeyMIC && !keyInfo.Secure)? HandshakeStage[2] :
( keyInfo.Install && keyInfo.KeyACK && keyInfo.KeyMIC && keyInfo.Secure)? HandshakeStage[3] :
(!keyInfo.Install && !keyInfo.KeyACK && keyInfo.KeyMIC && keyInfo.Secure)? HandshakeStage[4] :
null;
break; break;
} }
if(newPacket) packet = Object.assign(newPacket, packet); if(newPacket) packet = Object.assign(newPacket, packet);

View File

@@ -13,7 +13,7 @@ class RegexBlockStream extends Transform{
* @param {RegExp} matcher Block-match * @param {RegExp} matcher Block-match
* @param {boolean} withholdLastBlock When true, the last matches block will not be submitted to prevent submitting incomplete blocks. * @param {boolean} withholdLastBlock When true, the last matches block will not be submitted to prevent submitting incomplete blocks.
* @param {boolean} matchAllOnFlush (Only in combination with withholdLastBlock) When enabled, the buffer will be matched on last time on _flush (stream deconstruction) and write any, also incomplete, blocks * @param {boolean} matchAllOnFlush (Only in combination with withholdLastBlock) When enabled, the buffer will be matched on last time on _flush (stream deconstruction) and write any, also incomplete, blocks
* @remarks WARNING: It should match a clean-block (including e.g. newline)! Otherwise buffer will get dirty and use more and more ressources. * @remarks WARNING: It should match a clean-block (including e.g. newline)! Otherwise buffer will get dirty and use more and more resources.
*/ */
constructor(matcher, withholdLastBlock = true, matchAllOnFlush = false){ constructor(matcher, withholdLastBlock = true, matchAllOnFlush = false){
super({ super({