UNPKG

ember-zli

Version:

Interact with EmberZNet-based adapters using zigbee-herdsman 'ember' driver

195 lines (194 loc) 8.89 kB
import { createSocket } from "node:dgram"; import { createWriteStream, existsSync } from "node:fs"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; import { confirm, input, select } from "@inquirer/prompts"; import { Command } from "@oclif/core"; import { ZSpec } from "zigbee-herdsman"; import { SLStatus } from "zigbee-herdsman/dist/adapter/ember/enums.js"; import { DATA_FOLDER, DEFAULT_PCAP_PATH, logger } from "../../index.js"; import { emberStart, emberStop } from "../../utils/ember.js"; import { getPortConf } from "../../utils/port.js"; import { browseToFile, computeCRC16CITTKermit } from "../../utils/utils.js"; import { createPcapFileHeader, createPcapPacketRecordMs, createWiresharkZEPFrame, PCAP_MAGIC_NUMBER_MS } from "../../utils/wireshark.js"; var SniffMenu; (function (SniffMenu) { SniffMenu[SniffMenu["START_SNIFFING"] = 0] = "START_SNIFFING"; })(SniffMenu || (SniffMenu = {})); const DEFAULT_WIRESHARK_IP_ADDRESS = "127.0.0.1"; const DEFAULT_ZEP_UDP_PORT = 17754; export default class Sniff extends Command { static args = {}; static description = "Sniff Zigbee traffic (to Wireshark, to PCAP file, to custom handler or just log raw data)."; static examples = ["<%= config.bin %> <%= command.id %>"]; static flags = {}; ezsp; sequence = 0; sniffing = false; udpSocket; pcapFileStream; wiresharkIPAddress = DEFAULT_WIRESHARK_IP_ADDRESS; zepUDPPort = DEFAULT_ZEP_UDP_PORT; customHandler; async run() { // 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 = 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(); this.pcapFileStream?.close(); await emberStop(this.ezsp); return this.exit(0); } async menuStartSniffing() { if (!this.ezsp) { logger.error("Invalid state, no EZSP layer available."); return this.exit(1); } let SniffDestination; (function (SniffDestination) { SniffDestination[SniffDestination["LOG_FILE"] = 0] = "LOG_FILE"; SniffDestination[SniffDestination["WIRESHARK"] = 1] = "WIRESHARK"; SniffDestination[SniffDestination["PCAP_FILE"] = 2] = "PCAP_FILE"; })(SniffDestination || (SniffDestination = {})); const sniffDestination = await select({ choices: [ { name: "Wireshark", value: SniffDestination.WIRESHARK, description: "Write to Wireshark ZEP UDP Protocol" }, { name: "PCAP file", value: SniffDestination.PCAP_FILE, description: "Write to a PCAP file for later use or sharing." }, { name: "Log", value: SniffDestination.LOG_FILE, description: "Write raw data to log file." }, ], message: "Destination (Note: if present, custom handler is always used, regardless of the selected destination)", }); switch (sniffDestination) { case SniffDestination.WIRESHARK: { 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); break; } case SniffDestination.PCAP_FILE: { const pcapFilePath = await browseToFile("PCAP file", DEFAULT_PCAP_PATH, true); this.pcapFileStream = createWriteStream(pcapFilePath, "utf8"); this.pcapFileStream.on("error", (error) => { logger.error(error); return true; }); const fileHeader = createPcapFileHeader(PCAP_MAGIC_NUMBER_MS); this.pcapFileStream.write(fileHeader); break; } } // set desired tx power before scan const radioTxPower = Number.parseInt(await input({ default: "5", message: "Radio transmit power [-128-127]", validate(value) { 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 channel = await select({ choices: ZSpec.ALL_802_15_4_CHANNELS.map((c) => ({ name: c.toString(), value: c })), 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, rssi, packetContents) => { if (this.customHandler) { this.customHandler(this, logger, linkQuality, rssi, packetContents); } switch (sniffDestination) { case SniffDestination.WIRESHARK: { try { const wsZEPFrame = createWiresharkZEPFrame(channel, deviceId, linkQuality, rssi, this.sequence, packetContents); this.sequence += 1; if (this.sequence > 0xffffffff) { // wrap if necessary... this.sequence = 0; } if (this.udpSocket) { this.udpSocket.send(wsZEPFrame, this.zepUDPPort, this.wiresharkIPAddress); } } catch (error) { logger.debug(error); } break; } case SniffDestination.PCAP_FILE: { if (this.pcapFileStream) { // fix static CRC used in EZSP >= v8 packetContents.set(computeCRC16CITTKermit(packetContents.subarray(0, -2)), packetContents.length - 2); const packet = createPcapPacketRecordMs(packetContents); this.pcapFileStream.write(packet); } break; } case SniffDestination.LOG_FILE: { ezspMfglibRxHandlerOriginal(linkQuality, rssi, packetContents); break; } } }; logger.info("Sniffing started."); return false; } async navigateMenu() { const answer = await select({ choices: [ { name: "Start sniffing", value: SniffMenu.START_SNIFFING, disabled: this.sniffing }, { name: "Exit", value: -1 }, ], message: "Menu", }); switch (answer) { case SniffMenu.START_SNIFFING: { return await this.menuStartSniffing(); } } return true; } }