xref: /aosp_15_r20/external/autotest/client/cros/netprotos/interface_host.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Copyright 2014 The Chromium OS Authors. All rights reserved.
2*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
3*9c5db199SXin Li# found in the LICENSE file.
4*9c5db199SXin Li
5*9c5db199SXin Liimport logging
6*9c5db199SXin Liimport socket
7*9c5db199SXin Liimport struct
8*9c5db199SXin Liimport time
9*9c5db199SXin Li
10*9c5db199SXin Lifrom autotest_lib.client.common_lib import error
11*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros.network import interface
12*9c5db199SXin Li
13*9c5db199SXin Li
14*9c5db199SXin Liclass InterfaceHost(object):
15*9c5db199SXin Li    """A host for use with ZeroconfDaemon that binds to an interface."""
16*9c5db199SXin Li
17*9c5db199SXin Li    @property
18*9c5db199SXin Li    def ip_addr(self):
19*9c5db199SXin Li        """Get the IP address of the interface we're bound to."""
20*9c5db199SXin Li        return self._interface.ipv4_address
21*9c5db199SXin Li
22*9c5db199SXin Li
23*9c5db199SXin Li    def __init__(self, interface_name):
24*9c5db199SXin Li        self._interface = interface.Interface(interface_name)
25*9c5db199SXin Li        self._socket = None
26*9c5db199SXin Li
27*9c5db199SXin Li
28*9c5db199SXin Li    def close(self):
29*9c5db199SXin Li        """Close the underlying socket."""
30*9c5db199SXin Li        if self._socket:
31*9c5db199SXin Li            self._socket.close()
32*9c5db199SXin Li
33*9c5db199SXin Li
34*9c5db199SXin Li    def socket(self, family, sock_type):
35*9c5db199SXin Li        """Get a socket bound to this interface.
36*9c5db199SXin Li
37*9c5db199SXin Li        Only supports IPv4 UDP sockets on broadcast addresses.
38*9c5db199SXin Li
39*9c5db199SXin Li        @param family: must be socket.AF_INET.
40*9c5db199SXin Li        @param sock_type: must be socket.SOCK_DGRAM.
41*9c5db199SXin Li
42*9c5db199SXin Li        """
43*9c5db199SXin Li        if family != socket.AF_INET or sock_type != socket.SOCK_DGRAM:
44*9c5db199SXin Li            raise error.TestError('InterfaceHost only understands UDP sockets.')
45*9c5db199SXin Li        if self._socket is not None:
46*9c5db199SXin Li            raise error.TestError('InterfaceHost only supports a single '
47*9c5db199SXin Li                                  'multicast socket.')
48*9c5db199SXin Li
49*9c5db199SXin Li        self._socket = InterfaceDatagramSocket(self.ip_addr)
50*9c5db199SXin Li        return self._socket
51*9c5db199SXin Li
52*9c5db199SXin Li
53*9c5db199SXin Li    def run_until(self, predicate, timeout_seconds):
54*9c5db199SXin Li        """Handle traffic from our socket until |predicate|() is true.
55*9c5db199SXin Li
56*9c5db199SXin Li        @param predicate: function without arguments that returns True or False.
57*9c5db199SXin Li        @param timeout_seconds: number of seconds to wait for predicate to
58*9c5db199SXin Li                                become True.
59*9c5db199SXin Li        @return: tuple(success, duration) where success is True iff predicate()
60*9c5db199SXin Li                 became true before |timeout_seconds| passed.
61*9c5db199SXin Li
62*9c5db199SXin Li        """
63*9c5db199SXin Li        start_time = time.time()
64*9c5db199SXin Li        duration = lambda: time.time() - start_time
65*9c5db199SXin Li        while duration() < timeout_seconds:
66*9c5db199SXin Li            if predicate():
67*9c5db199SXin Li                return True, duration()
68*9c5db199SXin Li            # Assume this take non-trivial time, don't sleep here.
69*9c5db199SXin Li            self._socket.run_once()
70*9c5db199SXin Li        return False, duration()
71*9c5db199SXin Li
72*9c5db199SXin Li
73*9c5db199SXin Liclass InterfaceDatagramSocket(object):
74*9c5db199SXin Li    """Broadcast UDP socket bound to a particular network interface."""
75*9c5db199SXin Li
76*9c5db199SXin Li    # Wait for a UDP frame to appear for this long before timing out.
77*9c5db199SXin Li    TIMEOUT_VALUE_SECONDS = 0.5
78*9c5db199SXin Li
79*9c5db199SXin Li    def __init__(self, interface_ip):
80*9c5db199SXin Li        """Construct an instance.
81*9c5db199SXin Li
82*9c5db199SXin Li        @param interface_ip: string like '239.192.1.100'.
83*9c5db199SXin Li
84*9c5db199SXin Li        """
85*9c5db199SXin Li        self._interface_ip = interface_ip
86*9c5db199SXin Li        self._recv_callback = None
87*9c5db199SXin Li        self._recv_sock = None
88*9c5db199SXin Li        self._send_sock = None
89*9c5db199SXin Li
90*9c5db199SXin Li
91*9c5db199SXin Li    def close(self):
92*9c5db199SXin Li        """Close state associated with this object."""
93*9c5db199SXin Li        if self._recv_sock is not None:
94*9c5db199SXin Li            # Closing the socket drops membership groups.
95*9c5db199SXin Li            self._recv_sock.close()
96*9c5db199SXin Li            self._recv_sock = None
97*9c5db199SXin Li        if self._send_sock is not None:
98*9c5db199SXin Li            self._send_sock.close()
99*9c5db199SXin Li            self._send_sock = None
100*9c5db199SXin Li
101*9c5db199SXin Li
102*9c5db199SXin Li    def listen(self, ip_addr, port, recv_callback):
103*9c5db199SXin Li        """Bind and listen on the ip_addr:port.
104*9c5db199SXin Li
105*9c5db199SXin Li        @param ip_addr: Multicast group IP (e.g. '224.0.0.251')
106*9c5db199SXin Li        @param port: Local destination port number.
107*9c5db199SXin Li        @param recv_callback: A callback function that accepts three arguments,
108*9c5db199SXin Li                              the received string, the sender IPv4 address and
109*9c5db199SXin Li                              the sender port number.
110*9c5db199SXin Li
111*9c5db199SXin Li        """
112*9c5db199SXin Li        if self._recv_callback is not None:
113*9c5db199SXin Li            raise error.TestError('listen() called twice on '
114*9c5db199SXin Li                                  'InterfaceDatagramSocket.')
115*9c5db199SXin Li        # Multicast addresses are in 224.0.0.0 - 239.255.255.255 (rfc5771)
116*9c5db199SXin Li        ip_addr_prefix = ord(socket.inet_aton(ip_addr)[0])
117*9c5db199SXin Li        if ip_addr_prefix < 224 or ip_addr_prefix > 239:
118*9c5db199SXin Li            raise error.TestError('Invalid multicast address.')
119*9c5db199SXin Li
120*9c5db199SXin Li        self._recv_callback = recv_callback
121*9c5db199SXin Li        # Set up a socket to receive just traffic from the given address.
122*9c5db199SXin Li        self._recv_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
123*9c5db199SXin Li        self._recv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
124*9c5db199SXin Li        self._recv_sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
125*9c5db199SXin Li                                   socket.inet_aton(ip_addr) +
126*9c5db199SXin Li                                   socket.inet_aton(self._interface_ip))
127*9c5db199SXin Li        self._recv_sock.settimeout(self.TIMEOUT_VALUE_SECONDS)
128*9c5db199SXin Li        self._recv_sock.bind((ip_addr, port))
129*9c5db199SXin Li        # When we send responses, we want to send them from this particular
130*9c5db199SXin Li        # interface.  The easiest way to do this is bind a socket directly to
131*9c5db199SXin Li        # the IP for the interface.  We're going to ignore messages sent to this
132*9c5db199SXin Li        # socket.
133*9c5db199SXin Li        self._send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
134*9c5db199SXin Li        self._send_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
135*9c5db199SXin Li        self._send_sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL,
136*9c5db199SXin Li                                   struct.pack('b', 1))
137*9c5db199SXin Li        self._send_sock.bind((self._interface_ip, port))
138*9c5db199SXin Li
139*9c5db199SXin Li
140*9c5db199SXin Li    def run_once(self):
141*9c5db199SXin Li        """Receive pending frames if available, return after timeout otw."""
142*9c5db199SXin Li        if self._recv_sock is None:
143*9c5db199SXin Li            raise error.TestError('Must listen() on socket before recv\'ing.')
144*9c5db199SXin Li        BUFFER_SIZE_BYTES = 2048
145*9c5db199SXin Li        try:
146*9c5db199SXin Li            data, sender_addr = self._recv_sock.recvfrom(BUFFER_SIZE_BYTES)
147*9c5db199SXin Li        except socket.timeout:
148*9c5db199SXin Li            return
149*9c5db199SXin Li        if len(sender_addr) != 2:
150*9c5db199SXin Li            logging.error('Unexpected address: %r', sender_addr)
151*9c5db199SXin Li        self._recv_callback(data, *sender_addr)
152*9c5db199SXin Li
153*9c5db199SXin Li
154*9c5db199SXin Li    def send(self, data, ip_addr, port):
155*9c5db199SXin Li        """Send |data| to an IPv4 address.
156*9c5db199SXin Li
157*9c5db199SXin Li        @param data: string of raw bytes to send.
158*9c5db199SXin Li        @param ip_addr: string like '239.192.1.100'.
159*9c5db199SXin Li        @param port: int like 50000.
160*9c5db199SXin Li
161*9c5db199SXin Li        """
162*9c5db199SXin Li        self._send_sock.sendto(data, (ip_addr, port))
163