# Copyright 2014 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import logging import socket import struct import time from autotest_lib.client.common_lib import error from autotest_lib.client.common_lib.cros.network import interface class InterfaceHost(object): """A host for use with ZeroconfDaemon that binds to an interface.""" @property def ip_addr(self): """Get the IP address of the interface we're bound to.""" return self._interface.ipv4_address def __init__(self, interface_name): self._interface = interface.Interface(interface_name) self._socket = None def close(self): """Close the underlying socket.""" if self._socket: self._socket.close() def socket(self, family, sock_type): """Get a socket bound to this interface. Only supports IPv4 UDP sockets on broadcast addresses. @param family: must be socket.AF_INET. @param sock_type: must be socket.SOCK_DGRAM. """ if family != socket.AF_INET or sock_type != socket.SOCK_DGRAM: raise error.TestError('InterfaceHost only understands UDP sockets.') if self._socket is not None: raise error.TestError('InterfaceHost only supports a single ' 'multicast socket.') self._socket = InterfaceDatagramSocket(self.ip_addr) return self._socket def run_until(self, predicate, timeout_seconds): """Handle traffic from our socket until |predicate|() is true. @param predicate: function without arguments that returns True or False. @param timeout_seconds: number of seconds to wait for predicate to become True. @return: tuple(success, duration) where success is True iff predicate() became true before |timeout_seconds| passed. """ start_time = time.time() duration = lambda: time.time() - start_time while duration() < timeout_seconds: if predicate(): return True, duration() # Assume this take non-trivial time, don't sleep here. self._socket.run_once() return False, duration() class InterfaceDatagramSocket(object): """Broadcast UDP socket bound to a particular network interface.""" # Wait for a UDP frame to appear for this long before timing out. TIMEOUT_VALUE_SECONDS = 0.5 def __init__(self, interface_ip): """Construct an instance. @param interface_ip: string like '239.192.1.100'. """ self._interface_ip = interface_ip self._recv_callback = None self._recv_sock = None self._send_sock = None def close(self): """Close state associated with this object.""" if self._recv_sock is not None: # Closing the socket drops membership groups. self._recv_sock.close() self._recv_sock = None if self._send_sock is not None: self._send_sock.close() self._send_sock = None def listen(self, ip_addr, port, recv_callback): """Bind and listen on the ip_addr:port. @param ip_addr: Multicast group IP (e.g. '224.0.0.251') @param port: Local destination port number. @param recv_callback: A callback function that accepts three arguments, the received string, the sender IPv4 address and the sender port number. """ if self._recv_callback is not None: raise error.TestError('listen() called twice on ' 'InterfaceDatagramSocket.') # Multicast addresses are in 224.0.0.0 - 239.255.255.255 (rfc5771) ip_addr_prefix = ord(socket.inet_aton(ip_addr)[0]) if ip_addr_prefix < 224 or ip_addr_prefix > 239: raise error.TestError('Invalid multicast address.') self._recv_callback = recv_callback # Set up a socket to receive just traffic from the given address. self._recv_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self._recv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self._recv_sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(ip_addr) + socket.inet_aton(self._interface_ip)) self._recv_sock.settimeout(self.TIMEOUT_VALUE_SECONDS) self._recv_sock.bind((ip_addr, port)) # When we send responses, we want to send them from this particular # interface. The easiest way to do this is bind a socket directly to # the IP for the interface. We're going to ignore messages sent to this # socket. self._send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self._send_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self._send_sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, struct.pack('b', 1)) self._send_sock.bind((self._interface_ip, port)) def run_once(self): """Receive pending frames if available, return after timeout otw.""" if self._recv_sock is None: raise error.TestError('Must listen() on socket before recv\'ing.') BUFFER_SIZE_BYTES = 2048 try: data, sender_addr = self._recv_sock.recvfrom(BUFFER_SIZE_BYTES) except socket.timeout: return if len(sender_addr) != 2: logging.error('Unexpected address: %r', sender_addr) self._recv_callback(data, *sender_addr) def send(self, data, ip_addr, port): """Send |data| to an IPv4 address. @param data: string of raw bytes to send. @param ip_addr: string like '239.192.1.100'. @param port: int like 50000. """ self._send_sock.sendto(data, (ip_addr, port))