1#!/usr/bin/env python3
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 os
31import socket
32
33
34class SnifferTransport(object):
35    """ Interface for transport that allows eavesdrop other nodes. """
36
37    def open(self):
38        """ Open transport.
39
40        Raises:
41            RuntimeError: when transport is already opened or when transport opening failed.
42        """
43        raise NotImplementedError
44
45    def close(self):
46        """ Close transport.
47
48        Raises:
49            RuntimeError: when transport is already closed.
50        """
51        raise NotImplementedError
52
53    @property
54    def is_opened(self):
55        """ Check if transport is opened.
56
57        Returns:
58            bool: True if the transport is opened, False in otherwise
59        """
60        raise NotImplementedError
61
62    def send(self, data, nodeid):
63        """ Send data to the node with nodeid.
64
65        Args:
66            data (bytearray): outcoming data.
67            nodeid (int): node id
68
69        Returns:
70            int: number of sent bytes
71        """
72        raise NotImplementedError
73
74    def recv(self, bufsize, timeout):
75        """ Receive data sent by other node.
76
77        Args:
78            bufsize (int): size of buffer for incoming data.
79            timeout (float | None): socket timeout.
80
81        Returns:
82            A tuple contains data and node id.
83
84            For example:
85            (bytearray([0x00, 0x01...], 1)
86
87        Raises:
88            socket.timeout: when receiving the packets times out.
89        """
90        raise NotImplementedError
91
92
93class SnifferSocketTransport(SnifferTransport):
94    """ Socket based implementation of sniffer transport. """
95
96    BASE_PORT = 9000
97
98    MAX_NETWORK_SIZE = int(os.getenv('MAX_NETWORK_SIZE', '33'))
99
100    PORT_OFFSET = int(os.getenv('PORT_OFFSET', '0'))
101
102    RADIO_GROUP = '224.0.0.116'
103
104    def __init__(self):
105        self._socket = None
106
107    def _nodeid_to_port(self, nodeid: int):
108        return self.BASE_PORT + (self.PORT_OFFSET * (self.MAX_NETWORK_SIZE + 1)) + nodeid
109
110    def _port_to_nodeid(self, port):
111        return (port - self.BASE_PORT - (self.PORT_OFFSET * (self.MAX_NETWORK_SIZE + 1)))
112
113    def open(self):
114        if self.is_opened:
115            raise RuntimeError('Transport is already opened.')
116
117        self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
118
119        if not self.is_opened:
120            raise RuntimeError('Transport opening failed.')
121
122        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
123        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
124        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 2 * 1024 * 1024)
125        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 2 * 1024 * 1024)
126        self._socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
127                                socket.inet_aton(self.RADIO_GROUP) + socket.inet_aton('127.0.0.1'))
128        self._socket.bind((self.RADIO_GROUP, self._nodeid_to_port(0)))
129
130    def close(self):
131        if not self.is_opened:
132            raise RuntimeError('Transport is closed.')
133
134        self._socket.close()
135        self._socket = None
136
137    @property
138    def is_opened(self):
139        return bool(self._socket is not None)
140
141    def send(self, data, nodeid):
142        address = ('127.0.0.1', self._nodeid_to_port(nodeid))
143
144        return self._socket.sendto(data, address)
145
146    def recv(self, bufsize, timeout):
147        self._socket.settimeout(timeout)
148        data, address = self._socket.recvfrom(bufsize)
149
150        nodeid = self._port_to_nodeid(address[1])
151
152        return bytearray(data), nodeid
153
154
155class SnifferTransportFactory(object):
156
157    def create_transport(self):
158        return SnifferSocketTransport()
159