Skip to content

Commit

Permalink
Add sniff to PCAP file support. Cleanup flags->interactive prompts.
Browse files Browse the repository at this point in the history
  • Loading branch information
Nerivec committed Sep 11, 2024
1 parent 28aab7b commit 0f54060
Show file tree
Hide file tree
Showing 10 changed files with 257 additions and 113 deletions.
19 changes: 7 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ $ npm install -g ember-zli
$ ember-zli COMMAND
running command...
$ ember-zli (--version)
ember-zli/2.3.1 win32-x64 node-v20.15.0
ember-zli/2.4.0 win32-x64 node-v20.15.0
$ ember-zli --help [COMMAND]
USAGE
$ ember-zli COMMAND
Expand All @@ -97,12 +97,7 @@ Interact with the Gecko bootloader in the adapter.

```
USAGE
$ ember-zli bootloader [-f <value>] [-r]
FLAGS
-f, --file=<value> Path to a firmware file. If not provided, will be set via interactive prompt when entering
relevant menu.
-r, --forceReset Try to force reset into bootloader (supported by: Sonoff ZBDongle-E).
$ ember-zli bootloader
DESCRIPTION
Interact with the Gecko bootloader in the adapter.
Expand All @@ -111,7 +106,7 @@ EXAMPLES
$ ember-zli bootloader
```

_See code: [src/commands/bootloader/index.ts](https://github.com/Nerivec/ember-zli/blob/v2.3.1/src/commands/bootloader/index.ts)_
_See code: [src/commands/bootloader/index.ts](https://github.com/Nerivec/ember-zli/blob/v2.4.0/src/commands/bootloader/index.ts)_

## `ember-zli help [COMMAND]`

Expand Down Expand Up @@ -148,7 +143,7 @@ EXAMPLES
$ ember-zli router
```

_See code: [src/commands/router/index.ts](https://github.com/Nerivec/ember-zli/blob/v2.3.1/src/commands/router/index.ts)_
_See code: [src/commands/router/index.ts](https://github.com/Nerivec/ember-zli/blob/v2.4.0/src/commands/router/index.ts)_

## `ember-zli sniff`

Expand All @@ -165,7 +160,7 @@ EXAMPLES
$ ember-zli sniff
```

_See code: [src/commands/sniff/index.ts](https://github.com/Nerivec/ember-zli/blob/v2.3.1/src/commands/sniff/index.ts)_
_See code: [src/commands/sniff/index.ts](https://github.com/Nerivec/ember-zli/blob/v2.4.0/src/commands/sniff/index.ts)_

## `ember-zli stack`

Expand All @@ -182,7 +177,7 @@ EXAMPLES
$ ember-zli stack
```

_See code: [src/commands/stack/index.ts](https://github.com/Nerivec/ember-zli/blob/v2.3.1/src/commands/stack/index.ts)_
_See code: [src/commands/stack/index.ts](https://github.com/Nerivec/ember-zli/blob/v2.4.0/src/commands/stack/index.ts)_

## `ember-zli utils`

Expand All @@ -199,7 +194,7 @@ EXAMPLES
$ ember-zli utils
```

_See code: [src/commands/utils/index.ts](https://github.com/Nerivec/ember-zli/blob/v2.3.1/src/commands/utils/index.ts)_
_See code: [src/commands/utils/index.ts](https://github.com/Nerivec/ember-zli/blob/v2.4.0/src/commands/utils/index.ts)_

## `ember-zli version`

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ember-zli",
"description": "Interact with EmberZNet-based adapters using zigbee-herdsman 'ember' driver",
"version": "2.3.1",
"version": "2.4.0",
"author": "Nerivec",
"bin": {
"ember-zli": "bin/run.js"
Expand Down
23 changes: 5 additions & 18 deletions src/commands/bootloader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { readdirSync, readFileSync } from 'node:fs'
import { join } from 'node:path'

import { input, select } from '@inquirer/prompts'
import { Command, Flags } from '@oclif/core'
import { Command } from '@oclif/core'
import { Presets, SingleBar } from 'cli-progress'

import { DATA_FOLDER, logger } from '../../index.js'
Expand All @@ -19,21 +19,8 @@ export default class Bootloader extends Command {
static override args = {}
static override description = 'Interact with the Gecko bootloader in the adapter.'
static override examples = ['<%= config.bin %> <%= command.id %>']
static override flags = {
file: Flags.file({
char: 'f',
description: 'Path to a firmware file. If not provided, will be set via interactive prompt when entering relevant menu.',
exists: true,
}),
forceReset: Flags.boolean({
char: 'r',
default: false,
description: 'Try to force reset into bootloader (supported by: Sonoff ZBDongle-E).',
}),
}

public async run(): Promise<void> {
const { flags } = await this.parse(Bootloader)
const portConf = await getPortConf()
logger.debug(`Using port conf: ${JSON.stringify(portConf)}`)

Expand Down Expand Up @@ -71,12 +58,12 @@ export default class Bootloader extends Command {
progressBar.update(percent)
})

await gecko.connect(flags.forceReset)
await gecko.connect()

let exit: boolean = false

while (!exit) {
exit = await this.navigateMenu(gecko, flags.file)
exit = await this.navigateMenu(gecko)
}

await gecko.transport.close(false)
Expand Down Expand Up @@ -104,7 +91,7 @@ export default class Bootloader extends Command {
return undefined
}

private async navigateMenu(gecko: GeckoBootloader, firmwareFile: string | undefined): Promise<boolean> {
private async navigateMenu(gecko: GeckoBootloader): Promise<boolean> {
const answer = await select<-1 | BootloaderMenu>({
choices: [
{ name: 'Get info', value: BootloaderMenu.INFO },
Expand All @@ -127,7 +114,7 @@ export default class Bootloader extends Command {
let validFirmware: FirmwareValidation = FirmwareValidation.INVALID

while (validFirmware !== FirmwareValidation.VALID) {
firmware = firmwareFile === undefined ? await this.selectFirmware(gecko) : readFileSync(firmwareFile)
firmware = await this.selectFirmware(gecko)

validFirmware = await gecko.validateFirmware(firmware, SUPPORTED_VERSIONS_REGEX)

Expand Down
104 changes: 81 additions & 23 deletions src/commands/sniff/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createSocket, Socket } from 'node:dgram'
import { existsSync } from 'node:fs'
import { createWriteStream, existsSync, WriteStream } from 'node:fs'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'

Expand All @@ -11,15 +11,22 @@ 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 { DATA_FOLDER, DEFAULT_PCAP_PATH, logger } from '../../index.js'
import { emberStart, emberStop } from '../../utils/ember.js'
import { getPortConf } from '../../utils/port.js'
import { createWiresharkZEPFrame } from '../../utils/wireshark.js'
import { browseToFile, computeCRC16CITTKermit } from '../../utils/utils.js'
import { createPcapFileHeader, createPcapPacketRecordMs, createWiresharkZEPFrame, PCAP_MAGIC_NUMBER_MS } from '../../utils/wireshark.js'

enum SniffMenu {
START_SNIFFING = 0,
}

enum SniffDestination {
LOG_FILE = 0,
WIRESHARK = 1,
PCAP_FILE = 2,
}

const DEFAULT_WIRESHARK_IP_ADDRESS = '127.0.0.1'
const DEFAULT_ZEP_UDP_PORT = 17754

Expand All @@ -33,6 +40,7 @@ export default class Sniff extends Command {
public sequence: number = 0
public sniffing: boolean = false
public udpSocket: Socket | undefined
public pcapFileStream: WriteStream | undefined
public wiresharkIPAddress: string = DEFAULT_WIRESHARK_IP_ADDRESS
public zepUDPPort: number = DEFAULT_ZEP_UDP_PORT

Expand All @@ -55,6 +63,7 @@ export default class Sniff extends Command {
}

this.udpSocket?.close()
this.pcapFileStream?.close()
await emberStop(this.ezsp)

return this.exit(0)
Expand All @@ -66,14 +75,42 @@ export default class Sniff extends Command {
return this.exit(1)
}

const sendToWireshark = await confirm({ message: 'Send to Wireshark?', default: true })
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)

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')
return true
})

this.udpSocket.bind(this.zepUDPPort)
const fileHeader = createPcapFileHeader(PCAP_MAGIC_NUMBER_MS)

this.pcapFileStream.write(fileHeader)

break
}
}

// set desired tx power before scan
Expand Down Expand Up @@ -156,24 +193,45 @@ export default class Sniff extends Command {
this.customHandler(this, logger, linkQuality, rssi, packetContents)
}

if (sendToWireshark) {
try {
const wsZEPFrame = createWiresharkZEPFrame(channel, deviceId, linkQuality, rssi, this.sequence, packetContents)
this.sequence += 1
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
}

if (this.sequence > 4294967295) {
// wrap if necessary...
this.sequence = 0
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)
}

// expected valid if `sendToWireshark`
this.udpSocket!.send(wsZEPFrame, this.zepUDPPort, this.wiresharkIPAddress)
} catch (error) {
logger.debug(error)
break
}

case SniffDestination.LOG_FILE: {
ezspMfglibRxHandlerOriginal(linkQuality, rssi, packetContents)

break
}
} else if (!this.customHandler) {
// log as debug if nothing enabled
ezspMfglibRxHandlerOriginal(linkQuality, rssi, packetContents)
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const DEFAULT_NETWORK_BACKUP_PATH = join(DATA_FOLDER, 'coordinator_backup
export const DEFAULT_TOKENS_BACKUP_PATH = join(DATA_FOLDER, 'tokens_backup.nvm3')
export const DEFAULT_ROUTER_TOKENS_BACKUP_PATH = join(DATA_FOLDER, 'router_tokens_backup.nvm3')

export const DEFAULT_PCAP_PATH = join(DATA_FOLDER, 'sniff.pcap')

if (!existsSync(DATA_FOLDER)) {
mkdirSync(DATA_FOLDER)
}
Expand Down
39 changes: 19 additions & 20 deletions src/utils/bootloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,16 @@ export class GeckoBootloader extends EventEmitter<GeckoBootloaderEventMap> {
this.xmodem.on(XEvent.DATA, this.onXModemData.bind(this))
}

public async connect(forceReset: boolean): Promise<void> {
public async connect(): Promise<void> {
if (this.state !== BootloaderState.NOT_CONNECTED) {
logger.debug(`Already connected to bootloader. Skipping connect attempt.`, NS)
return
}

logger.info(`Connecting to bootloader...`, NS)

// check if already in bootloader, or try to force into it if requested, don't fail if not successful
await this.knock(false, forceReset)
// check if already in bootloader, don't fail if not successful
await this.knock(false)

// @ts-expect-error changed by received serial data
if (this.state !== BootloaderState.IDLE) {
Expand Down Expand Up @@ -226,28 +226,22 @@ export class GeckoBootloader extends EventEmitter<GeckoBootloaderEventMap> {
}
}

public async resetByPattern(knock: boolean = false): Promise<void> {
if (!this.transport.isSerial) {
logger.error(`Reset by pattern unavailable for TCP.`, NS)
return
}

public async resetByPattern(exit: boolean): Promise<void> {
switch (this.adapterModel) {
// TODO: support per adapter
case 'Sonoff ZBDongle-E':
case undefined: {
case 'Sonoff ZBDongle-E': {
await this.transport.serialSet({ dtr: false, rts: true })
await this.transport.serialSet({ dtr: true, rts: false }, 100)

if (!knock) {
if (exit) {
await this.transport.serialSet({ dtr: false }, 500)
}

break
}

default: {
logger.error(`Reset by pattern unavailable on ${this.adapterModel}.`, NS)
logger.debug(`Reset by pattern unavailable for ${this.adapterModel}.`, NS)
}
}
}
Expand Down Expand Up @@ -380,18 +374,23 @@ export class GeckoBootloader extends EventEmitter<GeckoBootloaderEventMap> {
await emberStop(ezsp)
}

private async knock(fail: boolean, forceReset: boolean = false): Promise<void> {
private async knock(fail: boolean): Promise<void> {
logger.debug(`Knocking...`, NS)

try {
await this.transport.initPort()

if (forceReset) {
await this.resetByPattern(true)
// on first knock only, try pattern reset if supported
if (!fail && this.adapterModel === 'Sonoff ZBDongle-E') {
const forceReset = await confirm({ message: 'Force reset into bootloader?', default: true })

if (this.state === BootloaderState.IDLE) {
logger.debug(`Entered bootloader via pattern reset.`, NS)
return
if (forceReset) {
await this.resetByPattern(false)

if (this.state === BootloaderState.IDLE) {
logger.debug(`Entered bootloader via pattern reset.`, NS)
return
}
}
}
} catch (error) {
Expand Down Expand Up @@ -442,7 +441,7 @@ export class GeckoBootloader extends EventEmitter<GeckoBootloaderEventMap> {
// got menu back, failed to run
logger.warning(`Failed to exit bootloader and run firmware. Trying pattern reset...`, NS)

await this.resetByPattern()
await this.resetByPattern(true)
}

return true
Expand Down
Loading

0 comments on commit 0f54060

Please sign in to comment.