#!/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()