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