• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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