mirror of
https://github.com/espressif/openthread.git
synced 2026-08-03 01:17:46 +00:00
[harness] add functionality to discover and control simulations (#7901)
It can run all the Thread 1.1 test cases with simulations as a role of Router now except those requiring RF enclosure.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# Test Harness on Simulation Environment Setup
|
||||
|
||||
THCI (Thread Host Controller Interface) is an implementation of the Python abstract class template `IThci`, which is used by the Thread Test Harness Software to control OpenThread-based reference devices according to each test scenario.
|
||||
|
||||
SI (Sniffer Interface) is an implementation of the sniffer abstract class template `ISniffer`, which is used by the Thread Test Harness Software to sniff all packets sent by devices.
|
||||
|
||||
Both OpenThread simulation and sniffer simulation are required to run on a POSIX environment. However, Harness has to be run on Windows, which is a non-POSIX environment. So both two systems are needed, and their setup procedures in detail are listed in the following sections. Either two machines or one machine running two (sub)systems (for example, VM, WSL) is feasible.
|
||||
|
||||
Platform developers should modify the THCI implementation and/or the SI implementation directly to match their platform (for example, the path of the OpenThread repository).
|
||||
|
||||
## POSIX Environment Setup
|
||||
|
||||
1. Build OpenThread to generate standalone OpenThread simulation `ot-cli-ftd`. For example, run the following command in the top directory of OpenThread.
|
||||
```bash
|
||||
$ script/cmake-build simulation
|
||||
```
|
||||
Then `ot-cli-ftd` is built in the directory `build/simulation/examples/apps/cli/`.
|
||||
|
||||
## Test Harness Environment Setup
|
||||
|
||||
1. Double click the file `harness\install.bat` on the machine which installed Harness.
|
||||
|
||||
2. Check the configuration file `C:\GRL\Thread1.2\Thread_Harness\simulation\config.py`
|
||||
|
||||
- Edit the value of `REMOTE_USERNAME` to the username expected to connect to on the remote POSIX environment.
|
||||
- Edit the value of `REMOTE_PASSWORD` to the password corresponding to the username above.
|
||||
- Edit the value of `REMOTE_OT_PATH` to the absolute path where the top directory of the OpenThread repository is located.
|
||||
|
||||
3. Add the additional simulation device information in `harness\Web\data\deviceInputFields.xml` to `C:\GRL\Thread1.2\Web\data\deviceInputFields.xml`.
|
||||
|
||||
## Run Test Harness on Simulation
|
||||
|
||||
1. On POSIX machine, change directory to the top of OpenThread repository, and run the following commands.
|
||||
|
||||
```bash
|
||||
$ cd tools/harness-simulation/posix
|
||||
$ python harness_dev_discovery.py \
|
||||
--interface=eth0 \
|
||||
--ot1.1=24 \
|
||||
--sniffer=2
|
||||
```
|
||||
|
||||
It starts 24 OT FTD simulations and 2 sniffer simulations and can be discovered on eth0.
|
||||
|
||||
The arguments can be adjusted according to the requirement of test cases.
|
||||
|
||||
2. Run Test Harness. The information field of the device is encoded as `<node_id>@<ip_addr>`. Choose the proper device as the DUT accordingly.
|
||||
|
||||
3. Select one or more test cases to start the test.
|
||||
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2022, The OpenThread Authors.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. Neither the name of the copyright holder nor the
|
||||
# names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
import ipaddress
|
||||
import netifaces
|
||||
import os
|
||||
import paramiko
|
||||
import select
|
||||
import socket
|
||||
import struct
|
||||
import subprocess
|
||||
import time
|
||||
import winreg as wr
|
||||
|
||||
from ISniffer import ISniffer
|
||||
from THCI.OpenThread import watched
|
||||
from simulation.config import (
|
||||
REMOTE_PORT,
|
||||
REMOTE_USERNAME,
|
||||
REMOTE_PASSWORD,
|
||||
REMOTE_OT_PATH,
|
||||
REMOTE_SNIFFER_OUTPUT_PREFIX,
|
||||
EDITCAP_PATH,
|
||||
)
|
||||
|
||||
DISCOVERY_ADDR = ('ff02::114', 12345)
|
||||
|
||||
IFNAME = 'WLAN'
|
||||
|
||||
SCAN_TIME = 3
|
||||
|
||||
# `socket.IPPROTO_IPV6` only exists in Python 3, so the constant is manually defined.
|
||||
IPPROTO_IPV6 = 41
|
||||
|
||||
# The subkey represents the class of network adapter devices supported by the system,
|
||||
# which is used to filter physical network cards and avoid virtual network adapters.
|
||||
WINREG_KEY = r'SYSTEM\CurrentControlSet\Control\Network\{4d36e972-e325-11ce-bfc1-08002be10318}'
|
||||
|
||||
|
||||
class SimSniffer(ISniffer):
|
||||
|
||||
@watched
|
||||
def __init__(self, **kwargs):
|
||||
self.channel = kwargs.get('channel')
|
||||
self.ipaddr = kwargs.get('addressofDevice')
|
||||
self.is_active = False
|
||||
self._local_pcapng_location = None
|
||||
self._ssh = None
|
||||
self._remote_pcap_location = None
|
||||
self._remote_pid = None
|
||||
|
||||
def __repr__(self):
|
||||
return '%r' % self.__dict__
|
||||
|
||||
def _get_connection_name_from_guid(self, iface_guids):
|
||||
iface_names = ['(unknown)' for i in range(len(iface_guids))]
|
||||
reg = wr.ConnectRegistry(None, wr.HKEY_LOCAL_MACHINE)
|
||||
reg_key = wr.OpenKey(reg, WINREG_KEY)
|
||||
for i in range(len(iface_guids)):
|
||||
try:
|
||||
reg_subkey = wr.OpenKey(reg_key, iface_guids[i] + r'\Connection')
|
||||
iface_names[i] = wr.QueryValueEx(reg_subkey, 'Name')[0]
|
||||
except Exception, e:
|
||||
pass
|
||||
return iface_names
|
||||
|
||||
def _find_index(self, iface_name):
|
||||
ifaces_guid = netifaces.interfaces()
|
||||
absolute_iface_name = self._get_connection_name_from_guid(ifaces_guid)
|
||||
try:
|
||||
_required_iface_index = absolute_iface_name.index(iface_name)
|
||||
_required_guid = ifaces_guid[_required_iface_index]
|
||||
ip = netifaces.ifaddresses(_required_guid)[netifaces.AF_INET6][-1]['addr']
|
||||
self.log('Local IP: %s', ip)
|
||||
return int(ip.split('%')[1])
|
||||
except Exception, e:
|
||||
self.log('%r', e)
|
||||
self.log('Interface %s not found', iface_name)
|
||||
return None
|
||||
|
||||
def _encode_address_port(self, addr, port):
|
||||
port = str(port)
|
||||
if isinstance(ipaddress.ip_address(addr), ipaddress.IPv6Address):
|
||||
return '[' + addr + ']:' + port
|
||||
return addr + ':' + port
|
||||
|
||||
@watched
|
||||
def discoverSniffer(self):
|
||||
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
|
||||
# Define the output interface of the socket
|
||||
ifn = self._find_index(IFNAME)
|
||||
if ifn is None:
|
||||
self.log('%s interface has not enabled IPv6.', IFNAME)
|
||||
return []
|
||||
ifn = struct.pack('I', ifn)
|
||||
sock.setsockopt(IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, ifn)
|
||||
|
||||
# Send the request
|
||||
sock.sendto(('Sniffer').encode(), DISCOVERY_ADDR)
|
||||
|
||||
# Scan for responses
|
||||
devs = set()
|
||||
start = time.time()
|
||||
while time.time() - start < SCAN_TIME:
|
||||
if select.select([sock], [], [], 1)[0]:
|
||||
addr, _ = sock.recvfrom(1024)
|
||||
devs.add(addr)
|
||||
else:
|
||||
# Re-send the request, due to unreliability of UDP especially on WLAN
|
||||
sock.sendto(('Sniffer').encode(), DISCOVERY_ADDR)
|
||||
|
||||
devs = [SimSniffer(addressofDevice=addr, channel=None) for addr in devs]
|
||||
self.log('List of SimSniffers: %r', devs)
|
||||
|
||||
return devs
|
||||
|
||||
@watched
|
||||
def startSniffer(self, channelToCapture, captureFileLocation, includeEthernet=False):
|
||||
self.channel = channelToCapture
|
||||
self._local_pcapng_location = captureFileLocation
|
||||
self._remote_pcap_location = os.path.join(REMOTE_SNIFFER_OUTPUT_PREFIX, self.ipaddr.split('@')[0] + '.pcap')
|
||||
|
||||
self._ssh = paramiko.SSHClient()
|
||||
self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
remote_ip = self.ipaddr.split('@')[1]
|
||||
self._ssh.connect(remote_ip, port=REMOTE_PORT, username=REMOTE_USERNAME, password=REMOTE_PASSWORD)
|
||||
|
||||
_, stdout, _ = self._ssh.exec_command(
|
||||
'echo $$ && exec python3 %s -o %s -c %d' %
|
||||
(os.path.join(REMOTE_OT_PATH, 'tools/harness-simulation/posix/sniffer_sim/sniffer.py'),
|
||||
self._remote_pcap_location, self.channel))
|
||||
self._remote_pid = int(stdout.readline())
|
||||
|
||||
self.log('local pcapng location = %s', self._local_pcapng_location)
|
||||
self.log('remote pcap location = %s', self._remote_pcap_location)
|
||||
self.log('remote pid = %d', self._remote_pid)
|
||||
|
||||
self.is_active = True
|
||||
|
||||
@watched
|
||||
def stopSniffer(self):
|
||||
if not self.is_active:
|
||||
return
|
||||
self.is_active = False
|
||||
|
||||
assert self._ssh is not None
|
||||
self._ssh.exec_command('kill -s TERM %d' % self._remote_pid)
|
||||
# Wait to make sure the file is closed
|
||||
time.sleep(3)
|
||||
|
||||
# Truncate suffix from .pcapng to .pcap
|
||||
local_pcap_location = self._local_pcapng_location[:-2]
|
||||
|
||||
with self._ssh.open_sftp() as sftp:
|
||||
sftp.get(self._remote_pcap_location, local_pcap_location)
|
||||
|
||||
self._ssh.close()
|
||||
|
||||
cmd = [EDITCAP_PATH, '-F', 'pcapng', local_pcap_location, self._local_pcapng_location]
|
||||
self.log('running editcap: %r', cmd)
|
||||
subprocess.Popen(cmd).wait()
|
||||
self.log('editcap done')
|
||||
|
||||
self._local_pcapng_location = None
|
||||
self._ssh = None
|
||||
self._remote_pcap_location = None
|
||||
self._remote_pid = None
|
||||
|
||||
@watched
|
||||
def setChannel(self, channelToCapture):
|
||||
self.channel = channelToCapture
|
||||
|
||||
@watched
|
||||
def getChannel(self):
|
||||
return self.channel
|
||||
|
||||
@watched
|
||||
def validateFirmwareVersion(self, device):
|
||||
return True
|
||||
|
||||
@watched
|
||||
def isSnifferCapturing(self):
|
||||
return self.is_active
|
||||
|
||||
@watched
|
||||
def getSnifferAddress(self):
|
||||
return self.ipaddr
|
||||
|
||||
@watched
|
||||
def globalReset(self):
|
||||
pass
|
||||
|
||||
def log(self, fmt, *args):
|
||||
try:
|
||||
msg = fmt % args
|
||||
print('%s - %s - %s' % ('SimSniffer', time.strftime('%b %d %H:%M:%S'), msg))
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2022, The OpenThread Authors.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. Neither the name of the copyright holder nor the
|
||||
# names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
"""
|
||||
>> Thread Host Controller Interface
|
||||
>> Device : OpenThread_Sim THCI
|
||||
>> Class : OpenThread_Sim
|
||||
"""
|
||||
|
||||
import ipaddress
|
||||
import os
|
||||
import paramiko
|
||||
import socket
|
||||
import time
|
||||
|
||||
from IThci import IThci
|
||||
from OpenThread import OpenThreadTHCI, watched
|
||||
from simulation.config import REMOTE_OT_PATH
|
||||
|
||||
|
||||
class SSHHandle(object):
|
||||
|
||||
def __init__(self, ip, port, username, password, device, node_id):
|
||||
ipaddress.ip_address(ip)
|
||||
self.ip = ip
|
||||
self.port = int(port)
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.__handle = None
|
||||
self.__stdin = None
|
||||
self.__stdout = None
|
||||
self.__connect(device, node_id)
|
||||
|
||||
@watched
|
||||
def __connect(self, device, node_id):
|
||||
self.close()
|
||||
|
||||
self.__handle = paramiko.SSHClient()
|
||||
self.__handle.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
self.log('Connecting to %s:%s with username=%s', self.ip, self.port, self.username)
|
||||
self.__handle.connect(self.ip, port=self.port, username=self.username, password=self.password)
|
||||
except paramiko.AuthenticationException:
|
||||
if not self.password:
|
||||
self.__handle.get_transport().auth_none(self.username)
|
||||
else:
|
||||
raise Exception('Password error')
|
||||
|
||||
self.__stdin, self.__stdout, _ = self.__handle.exec_command(device + ' ' + str(node_id), get_pty=True)
|
||||
self.__stdout.channel.setblocking(0)
|
||||
|
||||
# Wait some time for initiation
|
||||
time.sleep(0.1)
|
||||
|
||||
@watched
|
||||
def close(self):
|
||||
if self.__handle is None:
|
||||
return
|
||||
self.__stdin.write('exit\n')
|
||||
# Wait some time for termination
|
||||
time.sleep(0.1)
|
||||
self.__handle.close()
|
||||
self.__stdin = None
|
||||
self.__stdout = None
|
||||
self.__handle = None
|
||||
|
||||
def send(self, cmd):
|
||||
self.__stdin.write(cmd)
|
||||
|
||||
def recv(self):
|
||||
try:
|
||||
return self.__stdout.readline().rstrip()
|
||||
except socket.timeout:
|
||||
return ''
|
||||
|
||||
def log(self, fmt, *args):
|
||||
try:
|
||||
msg = fmt % args
|
||||
print('%s - %s - %s' % (self.port, time.strftime('%b %d %H:%M:%S'), msg))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class OpenThread_Sim(OpenThreadTHCI, IThci):
|
||||
DEFAULT_COMMAND_TIMEOUT = 20
|
||||
|
||||
__handle = None
|
||||
|
||||
device = os.path.join(REMOTE_OT_PATH, 'build/simulation/examples/apps/cli/ot-cli-ftd')
|
||||
|
||||
@watched
|
||||
def _connect(self):
|
||||
# Only actually connect once.
|
||||
if self.__handle is None:
|
||||
assert self.connectType == 'ip'
|
||||
assert '@' in self.telnetIp
|
||||
self.log('SSH connecting ...')
|
||||
node_id, ssh_ip = self.telnetIp.split('@')
|
||||
self.__handle = SSHHandle(ssh_ip, self.telnetPort, self.telnetUsername, self.telnetPassword, self.device,
|
||||
node_id)
|
||||
|
||||
self.log('connected to %s successfully', self.telnetIp)
|
||||
|
||||
@watched
|
||||
def _disconnect(self):
|
||||
pass
|
||||
|
||||
def _cliReadLine(self):
|
||||
tail = self.__handle.recv()
|
||||
return tail if tail else None
|
||||
|
||||
def _cliWriteLine(self, line):
|
||||
self.__handle.send(line + '\n')
|
||||
|
||||
def _onCommissionStart(self):
|
||||
pass
|
||||
|
||||
def _onCommissionStop(self):
|
||||
pass
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2022, The OpenThread Authors.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. Neither the name of the copyright holder nor the
|
||||
# names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
REMOTE_PORT = 22
|
||||
REMOTE_USERNAME = 'pi'
|
||||
REMOTE_PASSWORD = 'raspberry'
|
||||
|
||||
REMOTE_SNIFFER_OUTPUT_PREFIX = '/tmp/'
|
||||
|
||||
REMOTE_OT_PATH = '/home/pi/work/src/openthread-pr/'
|
||||
|
||||
EDITCAP_PATH = r'C:\Program Files (x86)\Wireshark_Thread\editcap.exe'
|
||||
@@ -0,0 +1,10 @@
|
||||
<DEVICE_FIELDS>
|
||||
<DEVICE name="OpenThread_Sim" thumbnail="OpenThread.png" description = "OpenThread Simulation" THCI="OpenThread_Sim">
|
||||
<ITEM label="Serial Line"
|
||||
type="text"
|
||||
forParam="SerialPort"
|
||||
validation="COM"
|
||||
hint="eg: COM1">COM1
|
||||
</ITEM>
|
||||
</DEVICE>
|
||||
</DEVICE_FIELDS>
|
||||
@@ -0,0 +1,2 @@
|
||||
xcopy /E /Y Thread_Harness %systemdrive%\GRL\Thread1.2\Thread_Harness
|
||||
copy /Y ..\..\harness-thci\OpenThread.py %systemdrive%\GRL\Thread1.2\Thread_Harness\THCI
|
||||
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2022, The OpenThread Authors.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. Neither the name of the copyright holder nor the
|
||||
# names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
import argparse
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
|
||||
GROUP = 'ff02::114'
|
||||
PORT = 12345
|
||||
MAX_OT11_NUM = 33
|
||||
MAX_SNIFFER_NUM = 4
|
||||
|
||||
|
||||
def if_nametoindex(ifname: str) -> int:
|
||||
libc = ctypes.CDLL(ctypes.util.find_library('c'))
|
||||
ret = libc.if_nametoindex(ifname.encode('ascii'))
|
||||
if not ret:
|
||||
raise RuntimeError('Invalid interface name')
|
||||
return ret
|
||||
|
||||
|
||||
def get_ipaddr(ifname: str) -> str:
|
||||
for line in os.popen(f'ip addr list dev {ifname} | grep inet | grep global'):
|
||||
addr = line.strip().split()[1]
|
||||
return addr.split('/')[0]
|
||||
raise RuntimeError(f'No IP address on dev {ifname}')
|
||||
|
||||
|
||||
def init_socket(ifname: str, group: str, port: int) -> socket.socket:
|
||||
# Look up multicast group address in name server and find out IP version
|
||||
addrinfo = socket.getaddrinfo(group, None)[0]
|
||||
assert addrinfo[0] == socket.AF_INET6
|
||||
|
||||
# Create a socket
|
||||
s = socket.socket(addrinfo[0], socket.SOCK_DGRAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, (ifname + '\0').encode('ascii'))
|
||||
|
||||
# Bind it to the port
|
||||
s.bind((group, port))
|
||||
|
||||
group_bin = socket.inet_pton(addrinfo[0], addrinfo[4][0])
|
||||
# Join group
|
||||
interface_index = if_nametoindex(ifname)
|
||||
mreq = group_bin + struct.pack('@I', interface_index)
|
||||
s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
|
||||
|
||||
return s
|
||||
|
||||
|
||||
def advertise_ftd(s: socket.socket, dst, ven: str, ver: str, add: str, por: int, number: int):
|
||||
# Node ID of ot-cli-ftd is 1-indexed
|
||||
for i in range(1, number + 1):
|
||||
info = {
|
||||
'ven': ven,
|
||||
'mod': f'{ven}_{i}',
|
||||
'ver': ver,
|
||||
'add': f'{i}@{add}',
|
||||
'por': por,
|
||||
}
|
||||
logging.info('Advertise: %r', info)
|
||||
s.sendto(json.dumps(info).encode('utf-8'), dst)
|
||||
|
||||
|
||||
def advertise_sniffer(s: socket.socket, dst, add: str, number: int):
|
||||
for i in range(number):
|
||||
info = 'Sniffer_%d@%s' % (i, add)
|
||||
logging.info('Advertise: %r', info)
|
||||
s.sendto(info.encode('utf-8'), dst)
|
||||
|
||||
|
||||
def main():
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# Parse arguments
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
# Determine the interface
|
||||
parser.add_argument('-i',
|
||||
'--interface',
|
||||
dest='ifname',
|
||||
type=str,
|
||||
required=True,
|
||||
help='the interface used for discovery')
|
||||
|
||||
# Determine the number of OpenThread 1.1 FTD simulations to be "detected" and then initiated
|
||||
parser.add_argument('--ot1.1',
|
||||
dest='ot11_num',
|
||||
type=int,
|
||||
required=False,
|
||||
default=0,
|
||||
help=f'the number of OpenThread FTD simulations, no more than {MAX_OT11_NUM}')
|
||||
|
||||
# Determine the number of sniffer simulations to be initiated and then detected
|
||||
parser.add_argument('-s',
|
||||
'--sniffer',
|
||||
dest='sniffer_num',
|
||||
type=int,
|
||||
required=False,
|
||||
default=0,
|
||||
help=f'the number of sniffer simulations, no more than {MAX_SNIFFER_NUM}')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check validation of arguments
|
||||
if not 0 <= args.ot11_num <= MAX_OT11_NUM:
|
||||
raise ValueError(f'The number of FTDs should be between 0 and {MAX_OT11_NUM}')
|
||||
|
||||
if not 0 <= args.sniffer_num <= MAX_SNIFFER_NUM:
|
||||
raise ValueError(f'The number of FTDs should be between 0 and {MAX_SNIFFER_NUM}')
|
||||
|
||||
if args.ot11_num == args.sniffer_num == 0:
|
||||
raise ValueError('At least one device is required')
|
||||
|
||||
# Get the local IP address on the specified interface
|
||||
addr = get_ipaddr(args.ifname)
|
||||
|
||||
s = init_socket(args.ifname, GROUP, PORT)
|
||||
|
||||
logging.info('Advertising on interface %s group %s ...', args.ifname, GROUP)
|
||||
|
||||
# Loop, printing any data we receive
|
||||
while True:
|
||||
data, src = s.recvfrom(64)
|
||||
|
||||
if data == b'BBR':
|
||||
logging.info('Received OpenThread simulation query, advertising')
|
||||
advertise_ftd(s, src, ven='OpenThread_Sim', ver='4', add=addr, por=22, number=args.ot11_num)
|
||||
|
||||
elif data == b'Sniffer':
|
||||
logging.info('Received sniffer simulation query, advertising')
|
||||
advertise_sniffer(s, src, add=addr, number=args.sniffer_num)
|
||||
|
||||
else:
|
||||
logging.warning('Received %r, but ignored', data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2022, The OpenThread Authors.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. Neither the name of the copyright holder nor the
|
||||
# names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
""" Module to provide codec utilities for .pcap formatters. """
|
||||
|
||||
import struct
|
||||
import time
|
||||
|
||||
# https://www.tcpdump.org/linktypes.html
|
||||
DLT_IEEE802_15_4_WITHFCS = 195
|
||||
|
||||
PCAP_MAGIC_NUMBER = 0xA1B2C3D4
|
||||
PCAP_VERSION_MAJOR = 2
|
||||
PCAP_VERSION_MINOR = 4
|
||||
|
||||
|
||||
class PcapCodec(object):
|
||||
""" Utility class for .pcap formatters. """
|
||||
|
||||
def __init__(self, filename, channel):
|
||||
self._dlt = DLT_IEEE802_15_4_WITHFCS
|
||||
if not filename.endswith('.pcap'):
|
||||
raise ValueError('Filename should end with .pcap')
|
||||
self._pcap_file = open(filename, 'wb')
|
||||
self._pcap_file.write(self.encode_header())
|
||||
self._channel = channel
|
||||
|
||||
def encode_header(self):
|
||||
""" Return a pcap file header. """
|
||||
return struct.pack(
|
||||
'<LHHLLLL',
|
||||
PCAP_MAGIC_NUMBER,
|
||||
PCAP_VERSION_MAJOR,
|
||||
PCAP_VERSION_MINOR,
|
||||
0,
|
||||
0,
|
||||
256,
|
||||
self._dlt,
|
||||
)
|
||||
|
||||
def encode_frame(self, frame, sec, usec):
|
||||
""" Return a pcap encapsulation of the given frame. """
|
||||
|
||||
# Ignore the first byte storing channel.
|
||||
frame = frame[1:]
|
||||
|
||||
length = len(frame)
|
||||
pcap_frame = struct.pack('<LLLL', sec, usec, length, length)
|
||||
pcap_frame += frame
|
||||
return pcap_frame
|
||||
|
||||
def _get_timestamp(self):
|
||||
""" Return the internal timestamp. """
|
||||
timestamp = time.time()
|
||||
timestamp_sec = int(timestamp)
|
||||
timestamp_usec = int((timestamp - timestamp_sec) * 1000000)
|
||||
return timestamp_sec, timestamp_usec
|
||||
|
||||
def append(self, frame):
|
||||
""" Append a frame. """
|
||||
|
||||
# Filter channel.
|
||||
if frame[0] != self._channel:
|
||||
return
|
||||
|
||||
timestamp = self._get_timestamp()
|
||||
pkt = self.encode_frame(frame, *timestamp)
|
||||
self._pcap_file.write(pkt)
|
||||
self._pcap_file.flush()
|
||||
|
||||
def close(self):
|
||||
""" Close the pcap file. """
|
||||
|
||||
self._pcap_file.close()
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2022, The OpenThread Authors.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. Neither the name of the copyright holder nor the
|
||||
# names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import signal
|
||||
import time
|
||||
import pcap_codec
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import sniffer_transport
|
||||
|
||||
|
||||
class Sniffer:
|
||||
""" Class representing the Sniffing node, whose main task is listening.
|
||||
"""
|
||||
|
||||
logger = logging.getLogger('sniffer.Sniffer')
|
||||
|
||||
RECV_BUFFER_SIZE = 4096
|
||||
|
||||
def __init__(self, filename, channel):
|
||||
self._pcap = pcap_codec.PcapCodec(filename, channel)
|
||||
|
||||
# Create transport
|
||||
transport_factory = sniffer_transport.SnifferTransportFactory()
|
||||
self._transport = transport_factory.create_transport()
|
||||
|
||||
self._thread = None
|
||||
self._thread_alive = threading.Event()
|
||||
self._thread_alive.clear()
|
||||
|
||||
def _sniffer_main_loop(self):
|
||||
""" Sniffer main loop. """
|
||||
|
||||
self.logger.debug('Sniffer started.')
|
||||
|
||||
while self._thread_alive.is_set():
|
||||
data, nodeid = self._transport.recv(self.RECV_BUFFER_SIZE)
|
||||
self._pcap.append(data)
|
||||
|
||||
self.logger.debug('Sniffer stopped.')
|
||||
|
||||
def start(self):
|
||||
""" Start sniffing. """
|
||||
|
||||
self._thread = threading.Thread(target=self._sniffer_main_loop)
|
||||
self._thread.daemon = True
|
||||
|
||||
self._transport.open()
|
||||
|
||||
self._thread_alive.set()
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
""" Stop sniffing. """
|
||||
|
||||
self._thread_alive.clear()
|
||||
|
||||
self._transport.close()
|
||||
|
||||
self._thread.join(timeout=1)
|
||||
self._thread = None
|
||||
|
||||
def close(self):
|
||||
""" Close the pcap file. """
|
||||
|
||||
self._pcap.close()
|
||||
|
||||
|
||||
def run_sniffer():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-o',
|
||||
'--output',
|
||||
dest='output',
|
||||
type=str,
|
||||
required=True,
|
||||
help='the path of the output .pcap file')
|
||||
parser.add_argument('-c',
|
||||
'--channel',
|
||||
dest='channel',
|
||||
type=int,
|
||||
required=True,
|
||||
help='the channel which is sniffered')
|
||||
args = parser.parse_args()
|
||||
|
||||
sniffer = Sniffer(args.output, args.channel)
|
||||
sniffer.start()
|
||||
|
||||
def atexit(signum, frame):
|
||||
sniffer.stop()
|
||||
sniffer.close()
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGTERM, atexit)
|
||||
|
||||
while sniffer._thread_alive.is_set():
|
||||
time.sleep(0.5)
|
||||
|
||||
sniffer.stop()
|
||||
sniffer.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_sniffer()
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2022, The OpenThread Authors.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. Neither the name of the copyright holder nor the
|
||||
# names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
import os
|
||||
import socket
|
||||
|
||||
|
||||
class SnifferTransport(object):
|
||||
""" Interface for transport that allows eavesdrop other nodes. """
|
||||
|
||||
def open(self):
|
||||
""" Open transport.
|
||||
|
||||
Raises:
|
||||
RuntimeError: when transport is already opened or when transport opening failed.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def close(self):
|
||||
""" Close transport.
|
||||
|
||||
Raises:
|
||||
RuntimeError: when transport is already closed.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def is_opened(self):
|
||||
""" Check if transport is opened.
|
||||
|
||||
Returns:
|
||||
bool: True if the transport is opened, False in otherwise
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def send(self, data, nodeid):
|
||||
""" Send data to the node with nodeid.
|
||||
|
||||
Args:
|
||||
data (bytearray): outcoming data.
|
||||
nodeid (int): node id
|
||||
|
||||
Returns:
|
||||
int: number of sent bytes
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def recv(self, bufsize):
|
||||
""" Receive data sent by other node.
|
||||
|
||||
Args:
|
||||
bufsize (int): size of buffer for incoming data.
|
||||
|
||||
Returns:
|
||||
A tuple contains data and node id.
|
||||
|
||||
For example:
|
||||
(bytearray([0x00, 0x01...], 1)
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SnifferSocketTransport(SnifferTransport):
|
||||
""" Socket based implementation of sniffer transport. """
|
||||
|
||||
BASE_PORT = 9000
|
||||
|
||||
MAX_NETWORK_SIZE = int(os.getenv('MAX_NETWORK_SIZE', '33'))
|
||||
|
||||
PORT_OFFSET = int(os.getenv('PORT_OFFSET', '0'))
|
||||
|
||||
RADIO_GROUP = '224.0.0.116'
|
||||
|
||||
def __init__(self):
|
||||
self._socket = None
|
||||
|
||||
def __del__(self):
|
||||
if not self.is_opened:
|
||||
return
|
||||
|
||||
self.close()
|
||||
|
||||
def _nodeid_to_port(self, nodeid: int):
|
||||
return self.BASE_PORT + (self.PORT_OFFSET * (self.MAX_NETWORK_SIZE + 1)) + nodeid
|
||||
|
||||
def _port_to_nodeid(self, port):
|
||||
return (port - self.BASE_PORT - (self.PORT_OFFSET * (self.MAX_NETWORK_SIZE + 1)))
|
||||
|
||||
def open(self):
|
||||
if self.is_opened:
|
||||
raise RuntimeError('Transport is already opened.')
|
||||
|
||||
self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
|
||||
if not self.is_opened:
|
||||
raise RuntimeError('Transport opening failed.')
|
||||
|
||||
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 2 * 1024 * 1024)
|
||||
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 2 * 1024 * 1024)
|
||||
self._socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
|
||||
socket.inet_aton(self.RADIO_GROUP) + socket.inet_aton('127.0.0.1'))
|
||||
self._socket.bind((self.RADIO_GROUP, self._nodeid_to_port(0)))
|
||||
|
||||
def close(self):
|
||||
if not self.is_opened:
|
||||
raise RuntimeError('Transport is closed.')
|
||||
|
||||
self._socket.close()
|
||||
self._socket = None
|
||||
|
||||
@property
|
||||
def is_opened(self):
|
||||
return bool(self._socket is not None)
|
||||
|
||||
def send(self, data, nodeid):
|
||||
address = ('127.0.0.1', self._nodeid_to_port(nodeid))
|
||||
|
||||
return self._socket.sendto(data, address)
|
||||
|
||||
def recv(self, bufsize):
|
||||
data, address = self._socket.recvfrom(bufsize)
|
||||
|
||||
nodeid = self._port_to_nodeid(address[1])
|
||||
|
||||
return bytearray(data), nodeid
|
||||
|
||||
|
||||
class SnifferTransportFactory(object):
|
||||
|
||||
def create_transport(self):
|
||||
return SnifferSocketTransport()
|
||||
Reference in New Issue
Block a user