1#!/usr/bin/env python 2# 3# Copyright (c) 2022, The OpenThread Authors. 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions are met: 8# 1. Redistributions of source code must retain the above copyright 9# notice, this list of conditions and the following disclaimer. 10# 2. Redistributions in binary form must reproduce the above copyright 11# notice, this list of conditions and the following disclaimer in the 12# documentation and/or other materials provided with the distribution. 13# 3. Neither the name of the copyright holder nor the 14# names of its contributors may be used to endorse or promote products 15# derived from this software without specific prior written permission. 16# 17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27# POSSIBILITY OF SUCH DAMAGE. 28# 29 30import ipaddress 31import netifaces 32import os 33import paramiko 34import select 35import socket 36import struct 37import subprocess 38import time 39import winreg as wr 40 41from ISniffer import ISniffer 42from THCI.OpenThread import watched 43from simulation.config import ( 44 REMOTE_PORT, 45 REMOTE_USERNAME, 46 REMOTE_PASSWORD, 47 REMOTE_OT_PATH, 48 REMOTE_SNIFFER_OUTPUT_PREFIX, 49 EDITCAP_PATH, 50) 51 52DISCOVERY_ADDR = ('ff02::114', 12345) 53 54IFNAME = 'WLAN' 55 56SCAN_TIME = 3 57 58# `socket.IPPROTO_IPV6` only exists in Python 3, so the constant is manually defined. 59IPPROTO_IPV6 = 41 60 61# The subkey represents the class of network adapter devices supported by the system, 62# which is used to filter physical network cards and avoid virtual network adapters. 63WINREG_KEY = r'SYSTEM\CurrentControlSet\Control\Network\{4d36e972-e325-11ce-bfc1-08002be10318}' 64 65 66class SimSniffer(ISniffer): 67 68 @watched 69 def __init__(self, **kwargs): 70 self.channel = kwargs.get('channel') 71 self.ipaddr = kwargs.get('addressofDevice') 72 self.is_active = False 73 self._local_pcapng_location = None 74 self._ssh = None 75 self._remote_pcap_location = None 76 self._remote_pid = None 77 78 def __repr__(self): 79 return '%r' % self.__dict__ 80 81 def _get_connection_name_from_guid(self, iface_guids): 82 iface_names = ['(unknown)' for i in range(len(iface_guids))] 83 reg = wr.ConnectRegistry(None, wr.HKEY_LOCAL_MACHINE) 84 reg_key = wr.OpenKey(reg, WINREG_KEY) 85 for i in range(len(iface_guids)): 86 try: 87 reg_subkey = wr.OpenKey(reg_key, iface_guids[i] + r'\Connection') 88 iface_names[i] = wr.QueryValueEx(reg_subkey, 'Name')[0] 89 except Exception, e: 90 pass 91 return iface_names 92 93 def _find_index(self, iface_name): 94 ifaces_guid = netifaces.interfaces() 95 absolute_iface_name = self._get_connection_name_from_guid(ifaces_guid) 96 try: 97 _required_iface_index = absolute_iface_name.index(iface_name) 98 _required_guid = ifaces_guid[_required_iface_index] 99 ip = netifaces.ifaddresses(_required_guid)[netifaces.AF_INET6][-1]['addr'] 100 self.log('Local IP: %s', ip) 101 return int(ip.split('%')[1]) 102 except Exception, e: 103 self.log('%r', e) 104 self.log('Interface %s not found', iface_name) 105 return None 106 107 def _encode_address_port(self, addr, port): 108 port = str(port) 109 if isinstance(ipaddress.ip_address(addr), ipaddress.IPv6Address): 110 return '[' + addr + ']:' + port 111 return addr + ':' + port 112 113 @watched 114 def discoverSniffer(self): 115 sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) 116 117 # Define the output interface of the socket 118 ifn = self._find_index(IFNAME) 119 if ifn is None: 120 self.log('%s interface has not enabled IPv6.', IFNAME) 121 return [] 122 ifn = struct.pack('I', ifn) 123 sock.setsockopt(IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, ifn) 124 125 # Send the request 126 sock.sendto(('Sniffer').encode(), DISCOVERY_ADDR) 127 128 # Scan for responses 129 devs = set() 130 start = time.time() 131 while time.time() - start < SCAN_TIME: 132 if select.select([sock], [], [], 1)[0]: 133 addr, _ = sock.recvfrom(1024) 134 devs.add(addr) 135 else: 136 # Re-send the request, due to unreliability of UDP especially on WLAN 137 sock.sendto(('Sniffer').encode(), DISCOVERY_ADDR) 138 139 devs = [SimSniffer(addressofDevice=addr, channel=None) for addr in devs] 140 self.log('List of SimSniffers: %r', devs) 141 142 return devs 143 144 @watched 145 def startSniffer(self, channelToCapture, captureFileLocation, includeEthernet=False): 146 self.channel = channelToCapture 147 self._local_pcapng_location = captureFileLocation 148 self._remote_pcap_location = os.path.join(REMOTE_SNIFFER_OUTPUT_PREFIX, self.ipaddr.split('@')[0] + '.pcap') 149 150 self._ssh = paramiko.SSHClient() 151 self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 152 remote_ip = self.ipaddr.split('@')[1] 153 self._ssh.connect(remote_ip, port=REMOTE_PORT, username=REMOTE_USERNAME, password=REMOTE_PASSWORD) 154 155 _, stdout, _ = self._ssh.exec_command( 156 'echo $$ && exec python3 %s -o %s -c %d' % 157 (os.path.join(REMOTE_OT_PATH, 'tools/harness-simulation/posix/sniffer_sim/sniffer.py'), 158 self._remote_pcap_location, self.channel)) 159 self._remote_pid = int(stdout.readline()) 160 161 self.log('local pcapng location = %s', self._local_pcapng_location) 162 self.log('remote pcap location = %s', self._remote_pcap_location) 163 self.log('remote pid = %d', self._remote_pid) 164 165 self.is_active = True 166 167 @watched 168 def stopSniffer(self): 169 if not self.is_active: 170 return 171 self.is_active = False 172 173 assert self._ssh is not None 174 self._ssh.exec_command('kill -s TERM %d' % self._remote_pid) 175 # Wait to make sure the file is closed 176 time.sleep(3) 177 178 # Truncate suffix from .pcapng to .pcap 179 local_pcap_location = self._local_pcapng_location[:-2] 180 181 with self._ssh.open_sftp() as sftp: 182 sftp.get(self._remote_pcap_location, local_pcap_location) 183 184 self._ssh.close() 185 186 cmd = [EDITCAP_PATH, '-F', 'pcapng', local_pcap_location, self._local_pcapng_location] 187 self.log('running editcap: %r', cmd) 188 subprocess.Popen(cmd).wait() 189 self.log('editcap done') 190 191 self._local_pcapng_location = None 192 self._ssh = None 193 self._remote_pcap_location = None 194 self._remote_pid = None 195 196 @watched 197 def setChannel(self, channelToCapture): 198 self.channel = channelToCapture 199 200 @watched 201 def getChannel(self): 202 return self.channel 203 204 @watched 205 def validateFirmwareVersion(self, device): 206 return True 207 208 @watched 209 def isSnifferCapturing(self): 210 return self.is_active 211 212 @watched 213 def getSnifferAddress(self): 214 return self.ipaddr 215 216 @watched 217 def globalReset(self): 218 pass 219 220 def log(self, fmt, *args): 221 try: 222 msg = fmt % args 223 print('%s - %s - %s' % ('SimSniffer', time.strftime('%b %d %H:%M:%S'), msg)) 224 except Exception: 225 pass 226