170 lines
6.0 KiB
Python
170 lines
6.0 KiB
Python
|
|
#!/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()
|