#!/usr/bin/env python3 # # Copyright (c) 2019, The OpenThread Authors. # All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # import os import sys import tempfile import argparse import subprocess import threading import logging import re from spinel.stream import StreamOpen from spinel.const import SPINEL from spinel.codec import WpanApi from serial.tools.list_ports import comports from enum import Enum # Nodeid is required to execute ot-ncp-ftd for its sim radio socket port. # This is maximum that works for MacOS. DEFAULT_NODEID = 34 COMMON_BAUDRATE = [460800, 115200, 9600] class Config(Enum): CHANNEL = 0 BAUDRATE = 1 TAP = 2 class _StreamCloser: def __init__(self, stream): self._stream = stream def __enter__(self): return self._stream def __exit__(self, exc_type, exc_val, exc_tb): self._stream.close() def extcap_config(interface, option, extcap_version): """List Configuration for the given interface""" args = [] values = [] args.append( (Config.CHANNEL.value, '--channel', 'Channel', 'IEEE 802.15.4 channel', 'selector', '{required=true}{default=11}')) match = re.match(r'^(\d+)(\.\d+)*$', extcap_version) if match and int(match.group(1)) >= 3: args.append((Config.TAP.value, '--tap', 'IEEE 802.15.4 TAP (only for Wireshark3.0 and later)', 'IEEE 802.15.4 TAP', 'boolflag', '{default=yes}')) for arg in args: print('arg {number=%d}{call=%s}{display=%s}{tooltip=%s}{type=%s}%s' % arg) values = values + [(Config.CHANNEL.value, '%d' % i, '%d' % i, 'true' if i == 11 else 'false') for i in range(11, 27)] for value in values: print('value {arg=%d}{value=%s}{display=%s}{default=%s}' % value) def extcap_dlts(interface): """List DLTs for the given interface""" print( 'dlt {number=195}{name=IEEE802_15_4_WITHFCS}{display=IEEE 802.15.4 with FCS}' ) print('dlt {number=283}{name=IEEE802_15_4_TAP}{display=IEEE 802.15.4 TAP}') def serialopen(interface, log_file): """ Open serial to indentify OpenThread sniffer :param interface: string, eg: '/dev/ttyUSB0 - Zolertia Firefly platform', '/dev/ttyACM1 - nRF52840 OpenThread Device' """ sys.stdout = log_file sys.stderr = log_file interface = str(interface).split()[0] baudrate = None for speed in COMMON_BAUDRATE: with _StreamCloser(StreamOpen('u', interface, False, baudrate=speed)) as stream, \ WpanApi(stream, nodeid=DEFAULT_NODEID, timeout=0.1) as wpan_api: # result should not be None for both NCP and RCP result = wpan_api.prop_get_value( SPINEL.PROP_CAPS) # confirm OpenThread Sniffer # check whether or not is OpenThread Sniffer if result is not None: baudrate = speed break if baudrate is not None: if sys.platform == 'win32': # Wireshark only shows the value of key `display`('OpenThread Sniffer'). # Here intentionally appends interface in the end (e.g. 'OpenThread Sniffer: COM0'). print('interface {value=%s:%s}{display=OpenThread Sniffer %s}' % (interface, baudrate, interface), file=sys.__stdout__, flush=True) else: # On Linux or MacOS, wireshark will show the concatenation of the content of `display` # and `interface` by default (e.g. 'OpenThread Sniffer: /dev/ttyACM0'). print('interface {value=%s:%s}{display=OpenThread Sniffer}' % (interface, baudrate), file=sys.__stdout__, flush=True) def extcap_interfaces(): """List available interfaces to capture from""" log_file = open( os.path.join(tempfile.gettempdir(), 'extcap_ot_interfaces.log'), 'w') print( 'extcap {version=1.0.0}{display=OpenThread Sniffer}{help=https://github.com/openthread/pyspinel}' ) threads = [] for interface in comports(): th = threading.Thread(target=serialopen, args=(interface, log_file)) threads.append(th) th.start() for th in threads: th.join() def extcap_capture(interface, fifo, control_in, control_out, channel, tap): """Start the sniffer to capture packets""" # baudrate = detect_baudrate(interface) interface_port = str(interface).split(':')[0] interface_baudrate = str(interface).split(':')[1] with _StreamCloser(StreamOpen('u', interface_port, False, baudrate=int(interface_baudrate))) as stream, \ WpanApi(stream, nodeid=DEFAULT_NODEID) as wpan_api: wpan_api.prop_set_value(SPINEL.PROP_PHY_ENABLED, 1) if sys.platform == 'win32': python_path = subprocess.Popen( 'py -3 -c "import sys; print(sys.executable)"', stdout=subprocess.PIPE, shell=True, ).stdout.readline().decode().strip() sniffer_py = os.path.join(os.path.dirname(python_path), 'Scripts', 'sniffer.py') cmd = ['python', sniffer_py] else: cmd = ['sniffer.py'] cmd += [ '-c', channel, '-u', interface_port, '--crc', '--rssi', '-b', interface_baudrate, '-o', str(fifo), '--is-fifo', '--use-host-timestamp' ] if tap: cmd.append('--tap') subprocess.Popen(cmd).wait() def extcap_close_fifo(fifo): """"Close extcap fifo""" # This is apparently needed to workaround an issue on Windows/macOS # where the message cannot be read. (really?) fh = open(fifo, 'wb', 0) fh.close() if __name__ == '__main__': # Capture options parser = argparse.ArgumentParser( description='OpenThread Sniffer extcap plugin') # Extcap Arguments parser.add_argument('--extcap-interfaces', help='Provide a list of interfaces to capture from', action='store_true') parser.add_argument('--extcap-interface', help='Provide the interface to capture from') parser.add_argument('--extcap-dlts', help='Provide a list of dlts for the given interface', action='store_true') parser.add_argument( '--extcap-config', help='Provide a list of configurations for the given interface', action='store_true') parser.add_argument('--extcap-reload-option', help='Reload elements for the given option') parser.add_argument('--capture', help='Start the capture routine', action='store_true') parser.add_argument( '--fifo', help='Use together with capture to provide the fifo to dump data to') parser.add_argument( '--extcap-capture-filter', help='Used together with capture to provide a capture filter') parser.add_argument('--extcap-control-in', help='Used to get control messages from toolbar') parser.add_argument('--extcap-control-out', help='Used to send control messages to toolbar') parser.add_argument('--extcap-version', help='Wireshark Version') # Interface Arguments parser.add_argument('--channel', help='IEEE 802.15.4 capture channel [11-26]') parser.add_argument( '--tap', help='IEEE 802.15.4 TAP (only for Wireshark3.0 and later)', action='store_true') try: args, unknown = parser.parse_known_args() except argparse.ArgumentError as e: parser.exit('ERROR_ARG: %s' % str(e)) extcap_version = '' version_path = os.path.join(tempfile.gettempdir(), 'extcap_ot_version') if args.extcap_version: extcap_version = args.extcap_version with open(version_path, mode='w') as f: f.write(extcap_version) else: try: with open(version_path, mode='r') as f: extcap_version = f.read() except FileNotFoundError: pass if len(unknown) > 0: parser.exit('Sniffer %d unknown arguments given: %s' % (len(unknown), unknown)) if len(sys.argv) == 0: parser.print_help() parser.exit('No arguments given!') if not args.extcap_interfaces and args.extcap_interface is None: parser.exit( 'An interface must be provided or the selection must be displayed') if args.extcap_interfaces: extcap_interfaces() sys.exit(0) if args.extcap_config: extcap_config(args.extcap_interface, '', extcap_version) elif args.extcap_dlts: extcap_dlts(args.extcap_interface) elif args.capture: if args.fifo is None: parser.exit('The fifo must be provided to capture') try: extcap_capture(args.extcap_interface, args.fifo, args.extcap_control_in, args.extcap_control_out, args.channel, args.tap) except KeyboardInterrupt: pass except Exception as e: logging.exception(e) parser.exit('ERROR_INTERNAL') else: parser.print_help() parser.exit('ERROR_USAGE')