-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
import { confirm, input, select } from '@inquirer/prompts' | ||
import { Command } from '@oclif/core' | ||
import { Socket, createSocket } from 'node:dgram' | ||
import { existsSync } from 'node:fs' | ||
import { join } from 'node:path' | ||
import { pathToFileURL } from 'node:url' | ||
import { Logger } from 'winston' | ||
import { ZSpec } from 'zigbee-herdsman' | ||
import { SLStatus } from 'zigbee-herdsman/dist/adapter/ember/enums.js' | ||
import { Ezsp } from 'zigbee-herdsman/dist/adapter/ember/ezsp/ezsp.js' | ||
|
||
import { DATA_FOLDER, logger } from '../../index.js' | ||
import { emberStart, emberStop } from '../../utils/ember.js' | ||
import { getPortConf } from '../../utils/port.js' | ||
import { createWiresharkZEPFrame } from '../../utils/wireshark.js' | ||
|
||
enum SniffMenu { | ||
START_SNIFFING = 0, | ||
} | ||
|
||
const DEFAULT_WIRESHARK_IP_ADDRESS = '127.0.0.1' | ||
const DEFAULT_ZEP_UDP_PORT = 17754 | ||
|
||
export default class Sniff extends Command { | ||
static override args = {} | ||
static override description = 'Sniff Zigbee traffic (to Wireshark, to custom handler or just log in file)' | ||
static override examples = ['<%= config.bin %> <%= command.id %>'] | ||
static override flags = {} | ||
|
||
public ezsp: Ezsp | undefined | ||
public sequence: number = 0 | ||
public sniffing: boolean = false | ||
public udpSocket: Socket | undefined | ||
public wiresharkIPAddress: string = DEFAULT_WIRESHARK_IP_ADDRESS | ||
public zepUDPPort: number = DEFAULT_ZEP_UDP_PORT | ||
|
||
private customHandler: ((cmd: Command, logger: Logger, linkQuality: number, rssi: number, packetContents: Buffer) => void) | undefined | ||
|
||
public async run(): Promise<void> { | ||
// const { args, flags } = await this.parse(Sniff) | ||
const portConf = await getPortConf() | ||
logger.debug(`Using port conf: ${JSON.stringify(portConf)}`) | ||
|
||
this.ezsp = await emberStart(portConf) | ||
let exit: boolean = false | ||
|
||
while (!exit) { | ||
exit = await this.navigateMenu() | ||
|
||
if (exit && this.sniffing) { | ||
exit = await confirm({ message: 'Sniffing is currently running. Confirm exit?', default: false }) | ||
} | ||
} | ||
|
||
this.udpSocket?.close() | ||
await emberStop(this.ezsp) | ||
|
||
return this.exit(0) | ||
} | ||
|
||
private async menuStartSniffing(): Promise<boolean> { | ||
if (!this.ezsp) { | ||
logger.error(`Invalid state, no EZSP layer available.`) | ||
return this.exit(1) | ||
} | ||
|
||
const sendToWireshark = await confirm({ message: 'Send to Wireshark?', default: true }) | ||
|
||
if (sendToWireshark) { | ||
this.wiresharkIPAddress = await input({ message: 'Wireshark IP address', default: DEFAULT_WIRESHARK_IP_ADDRESS }) | ||
this.zepUDPPort = Number.parseInt(await input({ message: 'Wireshark ZEP UDP port', default: `${DEFAULT_ZEP_UDP_PORT}` }), 10) | ||
this.udpSocket = createSocket('udp4') | ||
|
||
this.udpSocket.bind(this.zepUDPPort) | ||
} | ||
|
||
// set desired tx power before scan | ||
const radioTxPower = Number.parseInt( | ||
await input({ | ||
default: '5', | ||
message: 'Radio transmit power [-128-127]', | ||
validate(value: string) { | ||
if (/\./.test(value)) { | ||
return false | ||
} | ||
|
||
const v = Number.parseInt(value, 10) | ||
|
||
return v >= -128 && v <= 127 | ||
}, | ||
}), | ||
10, | ||
) | ||
|
||
let status = await this.ezsp.ezspSetRadioPower(radioTxPower) | ||
|
||
if (status !== SLStatus.OK) { | ||
logger.error(`Failed to set transmit power to ${radioTxPower} status=${SLStatus[status]}.`) | ||
return true | ||
} | ||
|
||
const channelChoices: { name: string; value: number }[] = [] | ||
|
||
for (const channel of ZSpec.ALL_802_15_4_CHANNELS) { | ||
channelChoices.push({ name: channel.toString(), value: channel }) | ||
} | ||
|
||
const channel = await select<number>({ | ||
choices: channelChoices, | ||
message: 'Channel to sniff', | ||
}) | ||
const eui64 = await this.ezsp.ezspGetEui64() | ||
const deviceId = Number.parseInt(eui64.slice(-4), 16) | ||
|
||
status = await this.ezsp.mfglibInternalStart(true) | ||
|
||
if (status !== SLStatus.OK) { | ||
logger.error(`Failed to start listening for packets with status=${SLStatus[status]}.`) | ||
return true | ||
} | ||
|
||
status = await this.ezsp.mfglibInternalSetChannel(channel) | ||
|
||
if (status !== SLStatus.OK) { | ||
logger.error(`Failed to set channel with status=${SLStatus[status]}.`) | ||
return true | ||
} | ||
|
||
this.sniffing = true | ||
|
||
const handlerFile = join(DATA_FOLDER, `ezspMfglibRxHandler.mjs`) | ||
|
||
if (existsSync(handlerFile)) { | ||
try { | ||
const importedScript = await import(pathToFileURL(handlerFile).toString()) | ||
|
||
if (typeof importedScript.default !== 'function') { | ||
throw new TypeError(`Not a function.`) | ||
} | ||
|
||
this.customHandler = importedScript.default | ||
|
||
logger.info(`Loaded custom handler.`) | ||
} catch (error) { | ||
logger.error(`Failed to load custom handler. ${error}`) | ||
} | ||
} | ||
|
||
// XXX: this is currently not restored, but not a problem since only possible menu is exit | ||
const ezspMfglibRxHandlerOriginal = this.ezsp.ezspMfglibRxHandler | ||
|
||
this.ezsp.ezspMfglibRxHandler = (linkQuality: number, rssi: number, packetContents: Buffer): void => { | ||
Check failure on line 152 in src/commands/sniff/index.ts
|
||
if (this.customHandler) { | ||
this.customHandler(this, logger, linkQuality, rssi, packetContents) | ||
} | ||
|
||
if (sendToWireshark) { | ||
try { | ||
const wsZEPFrame = createWiresharkZEPFrame(channel, deviceId, linkQuality, rssi, this.sequence, packetContents) | ||
this.sequence += 1 | ||
|
||
if (this.sequence > 4294967295) { | ||
// wrap if necessary... | ||
this.sequence = 0 | ||
} | ||
|
||
// expected valid if `sendToWireshark` | ||
this.udpSocket!.send(wsZEPFrame, this.zepUDPPort, this.wiresharkIPAddress) | ||
} catch (error) { | ||
logger.debug(error) | ||
} | ||
} else if (!this.customHandler) { | ||
// log as debug if nothing enabled | ||
ezspMfglibRxHandlerOriginal(linkQuality, rssi, packetContents) | ||
Check failure on line 174 in src/commands/sniff/index.ts
|
||
} | ||
} | ||
|
||
logger.info(`Sniffing started.`) | ||
|
||
return false | ||
} | ||
|
||
private async navigateMenu(): Promise<boolean> { | ||
const answer = await select<-1 | SniffMenu>({ | ||
choices: [ | ||
{ name: 'Start sniffing', value: SniffMenu.START_SNIFFING, disabled: this.sniffing }, | ||
{ name: 'Exit', value: -1 }, | ||
], | ||
message: 'Menu', | ||
}) | ||
|
||
switch (answer) { | ||
case SniffMenu.START_SNIFFING: { | ||
return this.menuStartSniffing() | ||
} | ||
} | ||
|
||
return true | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/** | ||
* @see https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-zep.c | ||
* @see https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-ieee802154.c | ||
* @see https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-zbee-nwk.c | ||
*------------------------------------------------------------ | ||
* | ||
* ZEP Packets must be received in the following format: | ||
* |UDP Header| ZEP Header |IEEE 802.15.4 Packet| | ||
* | 8 bytes | 16/32 bytes | <= 127 bytes | | ||
*------------------------------------------------------------ | ||
* | ||
* ZEP v1 Header will have the following format: | ||
* |Preamble|Version|Channel ID|Device ID|CRC/LQI Mode|LQI Val|Reserved|Length| | ||
* |2 bytes |1 byte | 1 byte | 2 bytes | 1 byte |1 byte |7 bytes |1 byte| | ||
* | ||
* ZEP v2 Header will have the following format (if type=1/Data): | ||
* |Preamble|Version| Type |Channel ID|Device ID|CRC/LQI Mode|LQI Val|NTP Timestamp|Sequence#|Reserved|Length| | ||
* |2 bytes |1 byte |1 byte| 1 byte | 2 bytes | 1 byte |1 byte | 8 bytes | 4 bytes |10 bytes|1 byte| | ||
* | ||
* ZEP v2 Header will have the following format (if type=2/Ack): | ||
* |Preamble|Version| Type |Sequence#| | ||
* |2 bytes |1 byte |1 byte| 4 bytes | | ||
*------------------------------------------------------------ | ||
*/ | ||
const PREAMBLE = 'EX' | ||
const PROTOCOL_VERSION = 2 | ||
const PROTOCOL_TYPE = 1 | ||
/** Baseline NTP time if bit-0=0 -> 7-Feb-2036 @ 06:28:16 UTC */ | ||
const MSB_0_BASE_TIME = 2085978496000n | ||
/** Baseline NTP time if bit-0=1 -> 1-Jan-1900 @ 01:00:00 UTC */ | ||
const MSB_1_BASE_TIME = -2208988800000n | ||
|
||
const getZepTimestamp = (): bigint => { | ||
const now = BigInt(Date.now()) | ||
const useBase1 = now < MSB_0_BASE_TIME // time < Feb-2036 | ||
// MSB_1_BASE_TIME: dates <= Feb-2036, MSB_0_BASE_TIME: if base0 needed for dates >= Feb-2036 | ||
const baseTime = now - (useBase1 ? MSB_1_BASE_TIME : MSB_0_BASE_TIME) | ||
let seconds = baseTime / 1000n | ||
const fraction = ((baseTime % 1000n) * 0x100000000n) / 1000n | ||
|
||
if (useBase1) { | ||
seconds |= 0x80000000n // set high-order bit if MSB_1_BASE_TIME 1900 used | ||
} | ||
|
||
return BigInt.asIntN(64, (seconds << 32n) | fraction) | ||
} | ||
|
||
export const createWiresharkZEPFrame = ( | ||
channelId: number, | ||
deviceId: number, | ||
lqi: number, | ||
rssi: number, | ||
sequence: number, | ||
data: Buffer, | ||
lqiMode: boolean = false, | ||
): Buffer => { | ||
const buffer = Buffer.alloc(167) | ||
let offset = 0 | ||
|
||
// The IEEE 802.15.4 packet encapsulated in the ZEP frame must have the "TI CC24xx" format | ||
// See figure 21 on page 24 of the CC2420 datasheet: https://www.ti.com/lit/ds/symlink/cc2420.pdf | ||
// So, two bytes must be added at the end: | ||
// * First byte: RSSI value as a signed 8 bits integer (range -128 to 127) | ||
// * Second byte: | ||
// - the most significant bit is set to 1 of the CRC of the frame is correct | ||
// - the 7 least significant bits contain the LQI value as a unsigned 7 bits integer (range 0 to 127) | ||
data[data.length - 2] = rssi | ||
data[data.length - 1] = 0x80 | ((lqi >> 1) & 0x7f) | ||
|
||
// Protocol ID String | Character string | 2.0.3 to 4.2.5 | ||
buffer.write(PREAMBLE, offset) | ||
offset += 2 | ||
|
||
// Protocol Version | Unsigned integer (8 bits) | 1.2.0 to 4.2.5 | ||
buffer.writeUInt8(PROTOCOL_VERSION, offset++) | ||
// Type | Unsigned integer (8 bits) | 1.2.0 to 1.8.15, 1.12.0 to 4.2.5 | ||
buffer.writeUInt8(PROTOCOL_TYPE, offset++) | ||
// Channel ID | Unsigned integer (8 bits) | 1.2.0 to 4.2.5 | ||
buffer.writeUInt8(channelId, offset++) | ||
// Device ID | Unsigned integer (16 bits) | 1.2.0 to 4.2.5 | ||
buffer.writeUint16BE(deviceId, offset) | ||
offset += 2 | ||
|
||
// LQI/CRC Mode | Boolean | 1.2.0 to 4.2.5 | ||
buffer.writeUInt8(lqiMode ? 1 : 0, offset++) | ||
// Link Quality Indication | Unsigned integer (8 bits) | 1.2.0 to 4.2.5 | ||
buffer.writeUInt8(lqi, offset++) | ||
|
||
// Timestamp | Date and time | 1.2.0 to 4.2.5 | ||
buffer.writeBigInt64BE(getZepTimestamp(), offset) | ||
offset += 8 | ||
|
||
// Sequence Number | Unsigned integer (32 bits) | 1.2.0 to 4.2.5 | ||
buffer.writeUint32BE(sequence, offset) | ||
offset += 4 | ||
|
||
// Reserved Fields | Byte sequence | 2.0.0 to 4.2.5 | ||
offset += 10 | ||
|
||
// Length | Unsigned integer (8 bits) | 1.2.0 to 4.2.5 | ||
buffer.writeUInt8(data.length, offset++) | ||
|
||
buffer.set(data, offset) | ||
offset += data.length | ||
|
||
return buffer.subarray(0, offset) // increased to "beyond last" above | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
describe('sniff', () => {}) |