class Sniffer: def __init__(self, interface): self.interface = interface self.sniffer = AsyncSniffer(iface=interface, prn=self.__packet_handler, store=False) self.handlers = dict() self.channel = None def add_packet_handler(self, handler): self.handlers[type(handler).__name__] = handler def remove_packet_handler(self, handler): del self.handlers[type(handler).__name__] def start(self): self.sniffer.start() def stop(self): try: self.sniffer.stop() except Scapy_Exception as e: logger.exception(e) self.sniffer.join() def __packet_handler(self, pkt): for _, handler in self.handlers.items(): handler(pkt)
class Sniffer: def __init__(self, interface): self.interface = interface self.sniffer = AsyncSniffer(iface=interface, prn=self.__packet_handler, store=False) self.handlers = dict() self.channel = None def set_channel(self, channel: int): self.channel = channel def add_packet_handler(self, handler): self.handlers[type(handler).__name__] = handler def remove_packet_handler(self, handler): del self.handlers[type(handler).__name__] def start(self): self.sniffer.start() def stop(self): self.sniffer.stop() self.sniffer.join() def __packet_handler(self, pkt): for k, handler in self.handlers.items(): handler(pkt)
def sniff(self): # type: () -> None from scapy.supersocket import StreamSocket ssock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ssock.bind((get_if_addr(self.optsniff.get("iface", conf.iface)), self.port)) ssock.listen() sniffers = [] try: while True: clientsocket, address = ssock.accept() print("%s connected" % repr(address)) sock = StreamSocket(clientsocket, self.cls) optsniff = self.optsniff.copy() optsniff["prn"] = functools.partial(self.reply, send_function=sock.send, address=address) del optsniff["iface"] sniffer = AsyncSniffer(opened_socket=sock, **optsniff) sniffer.start() sniffers.append((sniffer, sock)) finally: for (sniffer, sock) in sniffers: try: sniffer.stop() except Exception: pass sock.close() self.close() ssock.close()
def listen_for_tunnel_pkts(self): """ Listens for tunnel packets that are trapped to CPU These packets may be trapped if there is no neighbor info for the inner packet destination IP in the hardware. """ def _ping_inner_dst(packet): """ Pings the inner destination IP for an encapsulated packet Args: packet: The encapsulated packet received """ inner_packet_type = self.get_inner_pkt_type(packet) if inner_packet_type and packet[IP].dst == self_ip: cmds = ['timeout', '0.2', 'ping', '-c1', '-W1', '-i0', '-n', '-q'] if inner_packet_type == IPv6: cmds.append('-6') dst_ip = packet[IP].payload[inner_packet_type].dst cmds.append(dst_ip) logger.log_info("Running command '{}'".format(' '.join(cmds))) subprocess.run(cmds, stdout=subprocess.DEVNULL) self_ip, peer_ip = self.get_ipinip_tunnel_addrs() if self_ip is None or peer_ip is None: logger.log_notice('Could not get tunnel addresses from ' 'config DB, exiting...') return None packet_filter = 'host {} and host {}'.format(self_ip, peer_ip) logger.log_notice('Starting tunnel packet handler for {}' .format(packet_filter)) sniff_intfs = self.get_up_portchannels() logger.log_info("Listening on interfaces {}".format(sniff_intfs)) sniffer = AsyncSniffer( iface=sniff_intfs, filter=packet_filter, prn=_ping_inner_dst, store=0 ) sniffer.start() while True: msgs = self.wait_for_netlink_msgs() if self.sniffer_restart_required(msgs): sniffer.stop() sniff_intfs = self.get_up_portchannels() logger.log_notice('Restarting tunnel packet handler on ' 'interfaces {}'.format(sniff_intfs)) sniffer = AsyncSniffer( iface=sniff_intfs, filter=packet_filter, prn=_ping_inner_dst, store=0 ) sniffer.start()
def sniff_gen(**kwargs): pkts = queue.SimpleQueue() t = AsyncSniffer(store=False, prn=pkts.put, **kwargs) t.start() try: while True: yield pkts.get() except KeyboardInterrupt: pass t.stop()
class PacketSniffer: """ Wrapper around the 'AsyncSniffer' class from the Scapy project. """ def __init__(self, config, new_packets): self.config = config self.new_packets = new_packets self.sniffer = AsyncSniffer(iface=self.config.interface, filter=self.config.generate_frame_filter(), store=False, prn=self.new_packet) def start(self): """ Starts the packet sniffer. """ self.sniffer.start() def stop(self): """ Stops the packet sniffer. """ self.sniffer.stop() def is_running(self): """ Returns true if the sniffer is running, false otherwise. """ return self.sniffer.running def new_packet(self, packet): """ Adds the packet given as parameter to the queue to be processed by the parser. """ self.new_packets.put(packet)
class DhcpClient(unittest.TestCase): def setUp(self): self._br = "dh_br0" self.vlan_sw = "vlan_sw" self.up_link_port = "" try: subprocess.check_call(["pkill", "dnsmasq"]) except subprocess.CalledProcessError: pass self.dhcp_wait = threading.Condition() self.pkt_list_lock = threading.Condition() self.dhcp_store = {} self.gw_info_map = {} self.gw_info = UplinkGatewayInfo(self.gw_info_map) self._sniffer = None self._last_xid = -1 def tearDown(self): self._dhcp_client.stop() BridgeTools.destroy_bridge(self._br) @unittest.skipIf(os.getuid(), reason="needs root user") def test_dhcp_lease1(self): self._setup_dhcp_vlan_off() mac1 = MacAddress("11:22:33:44:55:66") self._validate_dhcp_alloc_renew(mac1, None) """ Network diagram of vlan test setup VETH pair +---------------------+ ================================== | Namespace 1 with | +--------------------+ DHCP server | | | | | +---------------------+ | +----------------+ Patch +--------------+ uplink- +--------+-------+ +---------------------+ | | Port | | iface | | VETH Pair | Namespace 2 with | | GTP_BR0 +----------+ UPLINK_BR0 +-------------+ VLAN_SW +------------+ DHCP server | | | | | | | | | +----------------+ +--------------+ +----------------+ +---------------------+ """ @unittest.skipIf(os.getuid(), reason="needs root user") def test_dhcp_vlan(self): vlan1: int = 2 mac1 = MacAddress("11:22:33:44:55:66") self._setup_vlan_network() self._setup_dhcp_on_vlan(vlan1) self._validate_dhcp_alloc_renew(mac1, vlan1) self._validate_ip_subnet(mac1, vlan1) self._release_ip(mac1, vlan1) @unittest.skip("needs more investigation.") def test_dhcp_vlan_multi(self): self._setup_vlan_network() vlan1 = 51 mac1 = MacAddress("11:22:33:44:55:66") vlan2 = 52 mac2 = MacAddress("22:22:33:44:55:66") vlan3 = 53 mac3 = MacAddress("11:22:33:44:55:66") self._setup_dhcp_on_vlan(vlan1) self._setup_dhcp_on_vlan(vlan2) self._setup_dhcp_on_vlan(vlan3) self._validate_dhcp_alloc_renew(mac1, vlan1) self._validate_dhcp_alloc_renew(mac2, vlan2) self._validate_dhcp_alloc_renew(mac3, vlan3) self._validate_ip_subnet(mac1, vlan1) self._validate_ip_subnet(mac2, vlan2) self._validate_ip_subnet(mac3, vlan3) self._release_ip(mac1, vlan1) self._release_ip(mac2, vlan2) self._release_ip(mac3, vlan3) def _validate_dhcp_alloc_renew(self, mac1: MacAddress, vlan: int): self._alloc_ip_address_from_dhcp(mac1, vlan) self._validate_req_state(mac1, DHCPState.REQUEST, vlan) self._validate_state_as_current(mac1, vlan) # trigger lease reneval before deadline self._last_xid = self._get_state_xid(mac1, vlan) LOG.debug("time: %s", datetime.datetime.now()) time1 = datetime.datetime.now() + datetime.timedelta(seconds=100) self._start_sniffer() with freeze_time(time1): LOG.debug("check req packets time: %s", datetime.datetime.now()) self._stop_sniffer_and_check(DHCPState.REQUEST, mac1, vlan) self._validate_req_state(mac1, DHCPState.REQUEST, vlan) self._validate_state_as_current(mac1, vlan) # trigger lease after deadline self._last_xid = self._get_state_xid(mac1, vlan) time2 = datetime.datetime.now() + datetime.timedelta(seconds=2000) self._start_sniffer() LOG.debug( "check discover packets time: %s", datetime.datetime.now(), ) with freeze_time(time2): LOG.debug("check discover after lease loss") self._stop_sniffer_and_check(DHCPState.DISCOVER, mac1, vlan) self._validate_req_state(mac1, DHCPState.REQUEST, vlan) self._validate_state_as_current(mac1, vlan) def _setup_dhcp_vlan_off(self): self.up_link_port = "cl1uplink_p0" setup_dhcp_server = SCRIPT_PATH + "scripts/setup-test-dhcp-srv.sh" subprocess.check_call([setup_dhcp_server, "cl1"]) setup_uplink_br = [ SCRIPT_PATH + "scripts/setup-uplink-br.sh", self._br, self.up_link_port, DHCP_IFACE, ] subprocess.check_call(setup_uplink_br) self._setup_dhclp_client() def _setup_vlan_network(self): self.up_link_port = "v_ul_0" setup_vlan_switch = SCRIPT_PATH + "scripts/setup-uplink-vlan-sw.sh" subprocess.check_call([setup_vlan_switch, self.vlan_sw, "v"]) setup_uplink_br = [ SCRIPT_PATH + "scripts/setup-uplink-br.sh", self._br, self.up_link_port, DHCP_IFACE, ] subprocess.check_call(setup_uplink_br) self._setup_dhclp_client() def _setup_dhclp_client(self): self._dhcp_client = DHCPClient( dhcp_wait=self.dhcp_wait, dhcp_store=self.dhcp_store, gw_info=self.gw_info, iface=DHCP_IFACE, lease_renew_wait_min=1, ) self._dhcp_client.run() def _setup_dhcp_on_vlan(self, vlan: int): setup_vlan_switch = SCRIPT_PATH + "scripts/setup-uplink-vlan-srv.sh" subprocess.check_call([setup_vlan_switch, self.vlan_sw, str(vlan)]) def _validate_req_state( self, mac: MacAddress, state: DHCPState, vlan: int, ): for x in range(RETRY_LIMIT): LOG.debug("wait for state: %d" % x) with self.dhcp_wait: dhcp1 = self.dhcp_store.get(mac.as_redis_key(vlan)) if state == DHCPState.RELEASE and dhcp1 is None: return if dhcp1.state_requested == state: return time.sleep(PKT_CAPTURE_WAIT) assert 0 def _get_state_xid(self, mac: MacAddress, vlan: int): with self.dhcp_wait: dhcp1 = self.dhcp_store.get(mac.as_redis_key(vlan)) return dhcp1.xid def _validate_state_as_current(self, mac: MacAddress, vlan: int): with self.dhcp_wait: dhcp1 = self.dhcp_store.get(mac.as_redis_key(vlan)) self.assert_( dhcp1.state == DHCPState.OFFER or dhcp1.state == DHCPState.ACK, ) def _alloc_ip_address_from_dhcp( self, mac: MacAddress, vlan: int, ) -> DHCPDescriptor: retry_count = 0 with self.dhcp_wait: dhcp_desc = None while (retry_count < 60 and (dhcp_desc is None or dhcp_desc.ip_is_allocated() is not True)): if retry_count % 5 == 0: self._dhcp_client.send_dhcp_packet( mac, vlan, DHCPState.DISCOVER, ) self.dhcp_wait.wait(timeout=1) dhcp_desc = self._dhcp_client.get_dhcp_desc(mac, vlan) retry_count = retry_count + 1 return dhcp_desc def _validate_ip_subnet(self, mac: MacAddress, vlan: int): # vlan is configured with subnet : 10.200.x.1 # router IP is 10.200.x.211 # x is vlan id exptected_subnet = ipaddress.ip_network("10.200.%s.0/24" % vlan) exptected_router_ip = ipaddress.ip_address("10.200.%s.211" % vlan) with self.dhcp_wait: dhcp1 = self.dhcp_store.get(mac.as_redis_key(vlan)) self.assertEqual(dhcp1.subnet, str(exptected_subnet)) self.assertEqual(dhcp1.router_ip, exptected_router_ip) self.assert_(ipaddress.ip_address(dhcp1.ip) in exptected_subnet) def _release_ip(self, mac: MacAddress, vlan: int): self._dhcp_client.release_ip_address(mac, vlan) time.sleep(PKT_CAPTURE_WAIT) self._validate_req_state(mac, DHCPState.RELEASE, vlan) def _handle_dhcp_req_packet(self, packet): if DHCP not in packet: return with self.pkt_list_lock: self.pkt_list.append(packet) def _start_sniffer(self): # drop dhclient requests, this would avoid lease # renewal after freezing time. subprocess.check_call([ "ovs-ofctl", "add-flow", self._br, "priority=100,in_port=" + self.up_link_port + ",action=drop", ]) time.sleep(PKT_CAPTURE_WAIT) self._sniffer = AsyncSniffer( iface=self._br, filter="udp and (port 67 or 68)", store=False, prn=self._handle_dhcp_req_packet, ) self.pkt_list = [] self._sniffer.start() LOG.debug("sniffer started") time.sleep(PKT_CAPTURE_WAIT) def _stop_sniffer_and_check(self, state: DHCPState, mac: MacAddress, vlan: int): LOG.debug("delete drop flow") subprocess.check_call([ "ovs-ofctl", "del-flows", self._br, "in_port=" + self.up_link_port, ]) for x in range(RETRY_LIMIT): LOG.debug("wait for pkt: %d" % x) time.sleep(PKT_CAPTURE_WAIT) with self.pkt_list_lock: for pkt in self.pkt_list: if DHCP in pkt: if vlan and (Dot1Q not in pkt or vlan != pkt[Dot1Q].vlan): continue if pkt[DHCP].options[0][1] == int(state) and \ pkt[Ether].src == str(mac): self._sniffer.stop() return LOG.debug("Failed check for dhcp packet: %s", state) with self.pkt_list_lock: for pkt in self.pkt_list: LOG.debug("DHCP pkt %s", pkt.show(dump=True)) # validate if any dhcp packet was sent. if state == DHCPState.DISCOVER: self.assertNotEqual( self._last_xid, self._get_state_xid(mac, vlan), )
class DhcpClient(unittest.TestCase): def setUp(self): self._br = "t_up_br0" try: subprocess.check_call(["pkill", "dnsmasq"]) except subprocess.CalledProcessError: pass setup_dhcp_server = SCRIPT_PATH + "scripts/setup-test-dhcp-srv.sh" subprocess.check_call([setup_dhcp_server, "t0"]) setup_uplink_br = [SCRIPT_PATH + "scripts/setup-uplink-br.sh", self._br, DHCP_IFACE, "8A:00:00:00:00:01"] subprocess.check_call(setup_uplink_br) self.dhcp_wait = threading.Condition() self.dhcp_store = {} self.gw_info_map = {} self.gw_info = UplinkGatewayInfo(self.gw_info_map) self._dhcp_client = DHCPClient(dhcp_wait=self.dhcp_wait, dhcp_store=self.dhcp_store, gw_info=self.gw_info, iface="t_dhcp0", lease_renew_wait_min=1) self._dhcp_client.run() def tearDown(self): self._dhcp_client.stop() BridgeTools.destroy_bridge(self._br) @unittest.skipIf(os.getuid(), reason="needs root user") def test_dhcp_lease1(self): self._setup_sniffer() mac1 = MacAddress("11:22:33:44:55:66") dhcp1 = self._alloc_ip_address_from_dhcp(mac1) self.assertEqual(dhcp1.state_requested, DHCPState.REQUEST) assert (dhcp1.state == DHCPState.OFFER or dhcp1.state == DHCPState.ACK) # trigger lease reneval before deadline time1 = datetime.datetime.now() + datetime.timedelta(seconds=100) self._start_sniffer() with freeze_time(time1): time.sleep(PKT_CAPTURE_WAIT) self._stop_sniffer_and_check(DHCPState.REQUEST, mac1) self.assertEqual(dhcp1.state_requested, DHCPState.REQUEST) assert (dhcp1.state == DHCPState.OFFER or dhcp1.state == DHCPState.ACK) # trigger lease after deadline time2 = datetime.datetime.now() + datetime.timedelta(seconds=200) self._start_sniffer() with freeze_time(time2): time.sleep(PKT_CAPTURE_WAIT) self._stop_sniffer_and_check(DHCPState.DISCOVER, mac1) self.assertEqual(dhcp1.state_requested, DHCPState.REQUEST) assert (dhcp1.state == DHCPState.OFFER or dhcp1.state == DHCPState.ACK) self._dhcp_client.release_ip_address(mac1) dhcp1 = self.dhcp_store.get(mac1.as_redis_key()) self.assertEqual(dhcp1.state_requested, DHCPState.RELEASE) def _alloc_ip_address_from_dhcp(self, mac: MacAddress) -> DHCPDescriptor: retry_count = 0 with self.dhcp_wait: dhcp_desc = None while (retry_count < 60 and (dhcp_desc is None or dhcp_desc.ip_is_allocated() is not True)): if retry_count % 5 == 0: self._dhcp_client.send_dhcp_packet(mac, DHCPState.DISCOVER) self.dhcp_wait.wait(timeout=1) dhcp_desc = self._dhcp_client.get_dhcp_desc(mac) retry_count = retry_count + 1 return dhcp_desc def _handle_dhcp_req_packet(self, packet): if DHCP not in packet: return self.pkt_list.append(packet) def _setup_sniffer(self): self._sniffer = AsyncSniffer(iface=DHCP_IFACE, filter="udp and (port 67 or 68)", prn=self._handle_dhcp_req_packet) def _start_sniffer(self): self.pkt_list = [] self._sniffer.start() time.sleep(.5) def _stop_sniffer_and_check(self, state: DHCPState, mac: MacAddress): self._sniffer.stop() for pkt in self.pkt_list: logging.debug("pkt: %s " % pkt.summary()) if DHCP in pkt: if pkt[DHCP].options[0][1] == int(state) and \ pkt[Ether].src == str(mac): return assert 0
class PingClient: '''Class representing the client''' def __init__(self, dst: str): self.dst = dst self.__last_received_sequence_number = -1 self.__last_generated_sequence_number = -1 self.__sniffer = AsyncSniffer(prn=self.__handle_filtered_pkts, filter=f'src {self.dst} and icmp') self.__received_count = 0 def __generate_next_packet(self): '''function to generate the next pkt to transmit''' self.__last_generated_sequence_number += 1 payload = ClientPayload("MY_PING", self.__last_generated_sequence_number) pkt = IP(dst=self.dst) / ICMP() / str(payload) return pkt def __handle_filtered_pkts(self, pkt): '''captured pkts handler''' received_time = time.time() payload = pkt[ICMP].load.decode('utf-8') if payload.startswith('<"MY_PING", '): _, seq_no, sent_time = payload.strip('>').split(',') rtt = round((received_time - float(sent_time)) * 1000, 4) if rtt > 1000: return if self.__last_received_sequence_number + 1 == int(seq_no): print( f'<{self.__last_generated_sequence_number}, {received_time}, {payload}, "Successfully_Received", {rtt} ms>' ) self.__received_count += 1 elif self.__last_received_sequence_number + 1 < int(seq_no): print( f'<{self.__last_generated_sequence_number}, {payload}, "Received_out_of_order">' ) else: return self.__last_received_sequence_number += 1 def start_transmission(self, count): self.__sniffer.start() time.sleep(1) try: while count != 0: count -= 1 pkt = self.__generate_next_packet() pkt_sent_time = time.time() send(pkt, verbose=False) if time.time() - pkt_sent_time < 1: time.sleep(TIME_INTERVAL - time.time() + pkt_sent_time) if self.__last_received_sequence_number != self.__last_generated_sequence_number: print( f'<{self.__last_generated_sequence_number}, "Timed_Out">' ) self.__last_received_sequence_number += 1 except KeyboardInterrupt: print('\nPacket Transmission Terminated!') self.__sniffer.stop() packet_loss = round( ((self.__last_generated_sequence_number + 1 - self.__received_count) / (self.__last_generated_sequence_number + 1)) * 100, 2) print( f'Packet Loss: {packet_loss}% \t | Packets Sent : {self.__last_generated_sequence_number + 1} \t | Packets Received : {self.__received_count}' )
class SIPpSniffer(object): """ Provides functionality to provide network capturing traffic based on a network interface and capturing filter """ def __init__(self, interface): self.__interface = interface self.__folder = None # Delegate implementation to scapy.AsyncSniffer. # We don't inherit from scapy.AsyncSniffer, because scapy.AsyncSniffer requires more arguments on construction, # than we are ready to provide on construction of SIPpSniffer. self.__impl = None logging.debug('Created sniffer for interface {0}'.format(interface)) def start(self, filter, folder): logging.debug( 'Starting sniffer for interface {0} and filter {1}'.format( self.__interface, filter)) assert (not self.__impl) # Although Scapy docs say we could pass iface=None to sniff on all, this doesn't actually work. # Scapy listens on 1st available interface. # Therefore we need to collect list of interfaces by ourselves. ifaces = Network.SIPpNetwork.get_interfaces() if ifaces: class started: flag = False cond = threading.Condition() def started_callback(): with started.cond: started.flag = True started.cond.notify() logging.debug('Started sniffer for interface {0}'.format( self.__interface)) self.__folder = folder self.__impl = AsyncSniffer( filter=filter, iface=list( ifaces ), # scapy waits for list(str), while we have set(str) started_callback=started_callback) self.__impl.start() # Need to wait for sniffing thread actually to start. # Otherwise we might start SIP dialogs when thread is not capturing yet, # and therefore miss to capture some initial messages. with started.cond: # Issue #35: Busy-loop wait. # Otherwise we could wait forever if Scapy threads terminates before setting the started.flag. # For example, due to exception inside it. while not started.flag: started.cond.wait(1) if not self.__impl.thread.is_alive(): raise Exception( "Scapy thread has terminated while waiting for it to start" ) else: logging.debug( 'Found no available real interfaces to sniff for dummy interface {0}' .format(self.__interface)) def stop(self): if self.__impl: logging.debug('Stopping sniffer for interface {0}'.format( self.__interface)) # Issue #1: We can't use just AsyncSniffer.stop(join=True), # because sometimes deadlock happens in cases when we stop immediately after start (see comment in Issue #1 for details). # Likely this is a bug in scapy, which doesn't expect this kind of usage. # # Initially here we just stopped AsyncSniffer with: # super().stop(join=True) # # I believe the issue was as follows: # Main thread: # - enters SIPpSniffer.start() # - starts Scapy thread and blocks on started.cond.wait() # - Main thread is paused # - Scapy thread is entered # Scapy thread: # - enters AsyncSniffer._run() # - calls started_callback(), which calls started.cond.notify(). # - Scapy thread is paused # - Main thread is resumed # Main thread: # - continues its work, quickly finishes and calls AsyncSniffer.stop() # - AsyncSniffer.stop() sets AsyncSniffer.continue_sniff = False # - blocks on self.thread.join() # - Main thread is paused # - Scapy thread is resumed # Scapy thread: # - sets self.continue_sniff = True # - enters sniffing loop # # Therefore, Main thread cancels Scapy thread by setting self.continue_sniff = False, # but due to very short run time of Main thread, Scapy thread hasn't entered its loop. # Scapy thread is then resumed and sets this flag back to True and enters the loop. # The loop runs forever. # # To workaround this, we call AsyncSniffer.stop() continuosly, to reliably cancel Scapy thread. # This way, we continuosly set the self.continue_sniff = False, until the Scapy thread terminates. while True: self.__impl.stop(join=False) self.__impl.join(timeout=1) if self.__impl.thread.isAlive(): logging.debug( 'Sniffing thread is still alive for interface {0}, re-trying stopping' .format(self.__interface)) else: break # Issue #58: Sort basing on timestamp time_sorted = sorted(self.__impl.results.res, key=lambda pkt: pkt.time) wrpcap(os.path.join(self.__folder, self.__interface + ".pcap"), time_sorted) # restore defaults to be able to start() again self.__impl = None self.__folder = None
class ISOTPSocketImplementation: """ Implementation of an ISOTP "state machine". Most of the ISOTP logic was taken from https://github.com/hartkopp/can-isotp/blob/master/net/can/isotp.c This class is separated from ISOTPSoftSocket to make sure the background thread can't hold a reference to ISOTPSoftSocket, allowing it to be collected by the GC. :param can_socket: a CANSocket instance, preferably filtering only can frames with identifier equal to did :param src_id: the CAN identifier of the sent CAN frames :param dst_id: the CAN identifier of the received CAN frames :param padding: If True, pads sending packets with 0x00 which not count to the payload. Does not affect receiving packets. :param extended_addr: Extended Address byte to be added at the beginning of every CAN frame _sent_ by this object. Can be None in order to disable extended addressing on sent frames. :param extended_rx_addr: Extended Address byte expected to be found at the beginning of every CAN frame _received_ by this object. Can be None in order to disable extended addressing on received frames. :param rx_block_size: Block Size byte to be included in every Control Flow Frame sent by this object. The default value of 0 means that all the data will be received in a single block. :param stmin: Time Minimum Separation byte to be included in every Control Flow Frame sent by this object. The default value of 0 indicates that the peer will not wait any time between sending frames. :param listen_only: Disables send of flow control frames """ def __init__(self, can_socket, # type: "CANSocket" src_id, # type: int dst_id, # type: int padding=False, # type: bool extended_addr=None, # type: Optional[int] extended_rx_addr=None, # type: Optional[int] rx_block_size=0, # type: int stmin=0, # type: int listen_only=False # type: bool ): # type: (...) -> None self.can_socket = can_socket self.dst_id = dst_id self.src_id = src_id self.padding = padding self.fc_timeout = 1 self.cf_timeout = 1 self.filter_warning_emitted = False self.extended_rx_addr = extended_rx_addr self.ea_hdr = b"" if extended_addr is not None: self.ea_hdr = struct.pack("B", extended_addr) self.listen_only = listen_only self.rxfc_bs = rx_block_size self.rxfc_stmin = stmin self.rx_queue = ObjectPipe() self.rx_len = -1 self.rx_buf = None # type: Optional[bytes] self.rx_sn = 0 self.rx_bs = 0 self.rx_idx = 0 self.rx_ts = 0.0 # type: Union[float, EDecimal] self.rx_state = ISOTP_IDLE self.txfc_bs = 0 self.txfc_stmin = 0 self.tx_gap = 0 self.tx_buf = None # type: Optional[bytes] self.tx_sn = 0 self.tx_bs = 0 self.tx_idx = 0 self.rx_ll_dl = 0 self.tx_state = ISOTP_IDLE self.tx_timeout_handle = None # type: Optional[TimeoutScheduler.Handle] # noqa: E501 self.rx_timeout_handle = None # type: Optional[TimeoutScheduler.Handle] # noqa: E501 self.rx_thread_started = Event() self.rx_thread = AsyncSniffer( store=False, opened_socket=can_socket, prn=self.on_can_recv, started_callback=self.rx_thread_started.set) self.tx_mutex = Lock() self.rx_mutex = Lock() self.send_mutex = Lock() self.tx_done = Event() self.tx_exception = None # type: Optional[str] self.tx_callbacks = [] # type: List[Callable[[], None]] self.rx_callbacks = [] # type: List[Callable[[bytes], None]] self.rx_thread.start() self.rx_thread_started.wait(5) def __del__(self): # type: () -> None self.close() def can_send(self, load): # type: (bytes) -> None if self.padding: load += b"\xCC" * (CAN_MAX_DLEN - len(load)) if self.src_id is None or self.src_id <= 0x7ff: self.can_socket.send(CAN(identifier=self.src_id, data=load)) else: self.can_socket.send(CAN(identifier=self.src_id, flags="extended", data=load)) def on_can_recv(self, p): # type: (Packet) -> None if not isinstance(p, CAN): raise Scapy_Exception("argument is not a CAN frame") if p.identifier != self.dst_id: if not self.filter_warning_emitted and conf.verb >= 2: warning("You should put a filter for identifier=%x on your " "CAN socket", self.dst_id) self.filter_warning_emitted = True else: self.on_recv(p) def close(self): # type: () -> None if self.rx_thread.thread and self.rx_thread.thread.is_alive(): self.rx_thread.stop(True) def _rx_timer_handler(self): # type: () -> None """Method called every time the rx_timer times out, due to the peer not sending a consecutive frame within the expected time window""" with self.rx_mutex: if self.rx_state == ISOTP_WAIT_DATA: # we did not get new data frames in time. # reset rx state self.rx_state = ISOTP_IDLE if conf.verb > 2: warning("RX state was reset due to timeout") def _tx_timer_handler(self): # type: () -> None """Method called every time the tx_timer times out, which can happen in two situations: either a Flow Control frame was not received in time, or the Separation Time Min is expired and a new frame must be sent.""" with self.tx_mutex: if (self.tx_state == ISOTP_WAIT_FC or self.tx_state == ISOTP_WAIT_FIRST_FC): # we did not get any flow control frame in time # reset tx state self.tx_state = ISOTP_IDLE self.tx_exception = "TX state was reset due to timeout" self.tx_done.set() raise Scapy_Exception(self.tx_exception) elif self.tx_state == ISOTP_SENDING: # push out the next segmented pdu src_off = len(self.ea_hdr) max_bytes = 7 - src_off if self.tx_buf is None: self.tx_exception = "TX buffer is not filled" raise Scapy_Exception(self.tx_exception) while 1: load = self.ea_hdr load += struct.pack("B", N_PCI_CF + self.tx_sn) load += self.tx_buf[self.tx_idx:self.tx_idx + max_bytes] self.can_send(load) self.tx_sn = (self.tx_sn + 1) % 16 self.tx_bs += 1 self.tx_idx += max_bytes if len(self.tx_buf) <= self.tx_idx: # we are done self.tx_state = ISOTP_IDLE self.tx_done.set() for cb in self.tx_callbacks: cb() return if self.txfc_bs != 0 and self.tx_bs >= self.txfc_bs: # stop and wait for FC self.tx_state = ISOTP_WAIT_FC self.tx_timeout_handle = TimeoutScheduler.schedule( self.fc_timeout, self._tx_timer_handler) return if self.tx_gap == 0: continue else: # stop and wait for tx gap self.tx_timeout_handle = TimeoutScheduler.schedule( self.tx_gap, self._tx_timer_handler) return def on_recv(self, cf): # type: (Packet) -> None """Function that must be called every time a CAN frame is received, to advance the state machine.""" data = bytes(cf.data) if len(data) < 2: return ae = 0 if self.extended_rx_addr is not None: ae = 1 if len(data) < 3: return if six.indexbytes(data, 0) != self.extended_rx_addr: return n_pci = six.indexbytes(data, ae) & 0xf0 if n_pci == N_PCI_FC: with self.tx_mutex: self._recv_fc(data[ae:]) elif n_pci == N_PCI_SF: with self.rx_mutex: self._recv_sf(data[ae:], cf.time) elif n_pci == N_PCI_FF: with self.rx_mutex: self._recv_ff(data[ae:], cf.time) elif n_pci == N_PCI_CF: with self.rx_mutex: self._recv_cf(data[ae:]) def _recv_fc(self, data): # type: (bytes) -> None """Process a received 'Flow Control' frame""" if (self.tx_state != ISOTP_WAIT_FC and self.tx_state != ISOTP_WAIT_FIRST_FC): return if self.tx_timeout_handle is not None: self.tx_timeout_handle.cancel() self.tx_timeout_handle = None if len(data) < 3: self.tx_state = ISOTP_IDLE self.tx_exception = "CF frame discarded because it was too short" self.tx_done.set() raise Scapy_Exception(self.tx_exception) # get communication parameters only from the first FC frame if self.tx_state == ISOTP_WAIT_FIRST_FC: self.txfc_bs = six.indexbytes(data, 1) self.txfc_stmin = six.indexbytes(data, 2) if ((self.txfc_stmin > 0x7F) and ((self.txfc_stmin < 0xF1) or (self.txfc_stmin > 0xF9))): self.txfc_stmin = 0x7F if six.indexbytes(data, 2) <= 127: tx_gap = six.indexbytes(data, 2) / 1000.0 elif 0xf1 <= six.indexbytes(data, 2) <= 0xf9: tx_gap = (six.indexbytes(data, 2) & 0x0f) / 10000.0 else: tx_gap = 0 self.tx_gap = tx_gap self.tx_state = ISOTP_WAIT_FC isotp_fc = six.indexbytes(data, 0) & 0x0f if isotp_fc == ISOTP_FC_CTS: self.tx_bs = 0 self.tx_state = ISOTP_SENDING # start cyclic timer for sending CF frame self.tx_timeout_handle = TimeoutScheduler.schedule( self.tx_gap, self._tx_timer_handler) elif isotp_fc == ISOTP_FC_WT: # start timer to wait for next FC frame self.tx_state = ISOTP_WAIT_FC self.tx_timeout_handle = TimeoutScheduler.schedule( self.fc_timeout, self._tx_timer_handler) elif isotp_fc == ISOTP_FC_OVFLW: # overflow in receiver side self.tx_state = ISOTP_IDLE self.tx_exception = "Overflow happened at the receiver side" self.tx_done.set() raise Scapy_Exception(self.tx_exception) else: self.tx_state = ISOTP_IDLE self.tx_exception = "Unknown FC frame type" self.tx_done.set() raise Scapy_Exception(self.tx_exception) def _recv_sf(self, data, ts): # type: (bytes, Union[float, EDecimal]) -> None """Process a received 'Single Frame' frame""" if self.rx_timeout_handle is not None: self.rx_timeout_handle.cancel() self.rx_timeout_handle = None if self.rx_state != ISOTP_IDLE: if conf.verb > 2: warning("RX state was reset because single frame was received") self.rx_state = ISOTP_IDLE length = six.indexbytes(data, 0) & 0xf if len(data) - 1 < length: return msg = data[1:1 + length] self.rx_queue.send((msg, ts)) for cb in self.rx_callbacks: cb(msg) def _recv_ff(self, data, ts): # type: (bytes, Union[float, EDecimal]) -> None """Process a received 'First Frame' frame""" if self.rx_timeout_handle is not None: self.rx_timeout_handle.cancel() self.rx_timeout_handle = None if self.rx_state != ISOTP_IDLE: if conf.verb > 2: warning("RX state was reset because first frame was received") self.rx_state = ISOTP_IDLE if len(data) < 7: return self.rx_ll_dl = len(data) # get the FF_DL self.rx_len = (six.indexbytes(data, 0) & 0x0f) * 256 + six.indexbytes( data, 1) ff_pci_sz = 2 # Check for FF_DL escape sequence supporting 32 bit PDU length if self.rx_len == 0: # FF_DL = 0 => get real length from next 4 bytes self.rx_len = six.indexbytes(data, 2) << 24 self.rx_len += six.indexbytes(data, 3) << 16 self.rx_len += six.indexbytes(data, 4) << 8 self.rx_len += six.indexbytes(data, 5) ff_pci_sz = 6 # copy the first received data bytes data_bytes = data[ff_pci_sz:] self.rx_idx = len(data_bytes) self.rx_buf = data_bytes self.rx_ts = ts # initial setup for this pdu reception self.rx_sn = 1 self.rx_state = ISOTP_WAIT_DATA # no creation of flow control frames if not self.listen_only: # send our first FC frame load = self.ea_hdr load += struct.pack("BBB", N_PCI_FC, self.rxfc_bs, self.rxfc_stmin) self.can_send(load) # wait for a CF self.rx_bs = 0 self.rx_timeout_handle = TimeoutScheduler.schedule( self.cf_timeout, self._rx_timer_handler) def _recv_cf(self, data): # type: (bytes) -> None """Process a received 'Consecutive Frame' frame""" if self.rx_state != ISOTP_WAIT_DATA: return if self.rx_timeout_handle is not None: self.rx_timeout_handle.cancel() self.rx_timeout_handle = None # CFs are never longer than the FF if len(data) > self.rx_ll_dl: return # CFs have usually the LL_DL length if len(data) < self.rx_ll_dl: # this is only allowed for the last CF if self.rx_len - self.rx_idx > self.rx_ll_dl: if conf.verb > 2: warning("Received a CF with insufficient length") return if six.indexbytes(data, 0) & 0x0f != self.rx_sn: # Wrong sequence number if conf.verb > 2: warning("RX state was reset because wrong sequence number was " "received") self.rx_state = ISOTP_IDLE return if self.rx_buf is None: raise Scapy_Exception("rx_buf not filled with data!") self.rx_sn = (self.rx_sn + 1) % 16 self.rx_buf += data[1:] self.rx_idx = len(self.rx_buf) if self.rx_idx >= self.rx_len: # we are done self.rx_buf = self.rx_buf[0:self.rx_len] self.rx_state = ISOTP_IDLE self.rx_queue.send((self.rx_buf, self.rx_ts)) for cb in self.rx_callbacks: cb(self.rx_buf) self.rx_buf = None return # perform blocksize handling, if enabled if self.rxfc_bs != 0: self.rx_bs += 1 # check if we reached the end of the block if self.rx_bs >= self.rxfc_bs and not self.listen_only: # send our FC frame load = self.ea_hdr load += struct.pack("BBB", N_PCI_FC, self.rxfc_bs, self.rxfc_stmin) self.can_send(load) # wait for another CF self.rx_timeout_handle = TimeoutScheduler.schedule( self.cf_timeout, self._rx_timer_handler) def begin_send(self, x): # type: (bytes) -> None """Begins sending an ISOTP message. This method does not block.""" with self.tx_mutex: if self.tx_state != ISOTP_IDLE: raise Scapy_Exception("Socket is already sending, retry later") self.tx_done.clear() self.tx_exception = None self.tx_state = ISOTP_SENDING length = len(x) if length > ISOTP_MAX_DLEN_2015: raise Scapy_Exception("Too much data for ISOTP message") if len(self.ea_hdr) + length <= 7: # send a single frame data = self.ea_hdr data += struct.pack("B", length) data += x self.tx_state = ISOTP_IDLE self.can_send(data) self.tx_done.set() for cb in self.tx_callbacks: cb() return # send the first frame data = self.ea_hdr if length > ISOTP_MAX_DLEN: data += struct.pack(">HI", 0x1000, length) else: data += struct.pack(">H", 0x1000 | length) load = x[0:8 - len(data)] data += load self.can_send(data) self.tx_buf = x self.tx_sn = 1 self.tx_bs = 0 self.tx_idx = len(load) self.tx_state = ISOTP_WAIT_FIRST_FC self.tx_timeout_handle = TimeoutScheduler.schedule( self.fc_timeout, self._tx_timer_handler) def send(self, p): # type: (bytes) -> None """Send an ISOTP frame and block until the message is sent or an error happens.""" with self.send_mutex: self.begin_send(p) # Wait until the tx callback is called send_done = self.tx_done.wait(30) if self.tx_exception is not None: raise Scapy_Exception(self.tx_exception) if not send_done: raise Scapy_Exception("ISOTP send not completed in 30s") return def recv(self, timeout=None): # type: (Optional[int]) -> Optional[Tuple[bytes, Union[float, EDecimal]]] # noqa: E501 """Receive an ISOTP frame, blocking if none is available in the buffer for at most 'timeout' seconds.""" try: return self.rx_queue.recv() except IndexError: return None
class TunnelPacketHandler(object): """ This class handles unroutable tunnel packets that are trapped to the CPU from the ASIC. """ def __init__(self): self.config_db = ConfigDBConnector() self.config_db.connect() self.state_db = SonicV2Connector() self.state_db.connect(STATE_DB) self._portchannel_intfs = None self.up_portchannels = None self.netlink_api = IPRoute() self.sniffer = None self.self_ip = '' self.packet_filter = '' self.sniff_intfs = [] @property def portchannel_intfs(self): """ Gets all portchannel interfaces and IPv4 addresses in config DB Returns: (list) Tuples of a portchannel interface name (str) and associated IPv4 address (str) """ if self._portchannel_intfs is None: intf_keys = self.config_db.get_keys(PORTCHANNEL_INTERFACE_TABLE) portchannel_intfs = [] for key in intf_keys: if isinstance(key, tuple) and len(key) > 1: if ip_interface(key[1]).version == 4: portchannel_intfs.append(key) self._portchannel_intfs = portchannel_intfs return self._portchannel_intfs def get_intf_name(self, msg): """ Gets the interface name for a netlink msg Returns: (str) The interface name, or the empty string if no interface name was found """ attr_list = msg.get('attrs', list()) for attribute, val in attr_list: if attribute == 'IFLA_IFNAME': return val return '' def netlink_msg_is_for_portchannel(self, msg): """ Determines if a netlink message is about a PortChannel interface Returns: (list) integers representing kernel indices """ ifname = self.get_intf_name(msg) return ifname in [name for name, _ in self.portchannel_intfs] def get_up_portchannels(self): """ Returns the portchannels which are operationally up Returns: (list) of interface names which are up, as strings """ portchannel_intf_names = [name for name, _ in self.portchannel_intfs] link_statuses = [] for intf in portchannel_intf_names: status = self.netlink_api.link("get", ifname=intf) link_statuses.append(status[0]) up_portchannels = [] for status in link_statuses: if status['state'] == 'up': up_portchannels.append(self.get_intf_name(status)) return up_portchannels def all_portchannels_established(self): """ Checks if the portchannel interfaces are established Note that this status does not indicate operational state Returns: (bool) True, if all interfaces are established False, otherwise """ intfs = self.portchannel_intfs for intf in intfs: intf_table_name = INTF_TABLE_TEMPLATE.format(intf[0], intf[1]) intf_state = self.state_db.get( STATE_DB, intf_table_name, STATE_KEY ) if intf_state and intf_state.lower() != 'ok': return False return True def wait_for_portchannels(self, interval=5, timeout=60): """ Continuosly checks if all portchannel host interfaces are established Args: interval: the interval (in seconds) at which to perform the check timeout: maximum allowed duration (in seconds) to wait for interfaces to come up Raises: RuntimeError if the timeout duration is reached and interfaces are still not up """ start = datetime.now() while (datetime.now() - start).seconds < timeout: if self.all_portchannels_established(): logger.log_info("All portchannel intfs are established") return None logger.log_info("Not all portchannel intfs are established") time.sleep(interval) raise RuntimeError('Portchannel intfs were not established ' 'within {}'.format(timeout)) def get_ipinip_tunnel_addrs(self): """ Get the IP addresses used for the IPinIP tunnel These should be the Loopback0 addresses for this device and the peer device Returns: ((str) self_loopback_ip, (str) peer_loopback_ip) or (None, None) If the tunnel type is not IPinIP or if an error is encountered. This most likely means the host device is not a dual ToR device """ try: peer_switch = self.config_db.get_keys(PEER_SWITCH_TABLE)[0] tunnel = self.config_db.get_keys(TUNNEL_TABLE)[0] except IndexError: logger.log_warning('PEER_SWITCH or TUNNEL table' 'not found in config DB') return None, None try: tunnel_table = self.config_db.get_entry(TUNNEL_TABLE, tunnel) tunnel_type = tunnel_table[TUNNEL_TYPE_KEY].lower() self_loopback_ip = tunnel_table[DST_IP_KEY] peer_loopback_ip = self.config_db.get_entry( PEER_SWITCH_TABLE, peer_switch )[ADDRESS_IPV4_KEY] except KeyError as error: logger.log_warning( 'PEER_SWITCH or TUNNEL table missing data, ' 'could not find key {}' .format(error) ) return None, None if tunnel_type == IPINIP_TUNNEL: return self_loopback_ip, peer_loopback_ip return None, None def get_inner_pkt_type(self, packet): """ Get the type of an inner encapsulated packet Returns: (str) 'v4' if the inner packet is IPv4 (str) 'v6' if the inner packet is IPv6 (bool) False if `packet` is not an IPinIP packet """ if packet.haslayer(IP): # Determine inner packet type based on IP protocol number # The outer packet type should always be IPv4 if packet[IP].proto == 4: return IP elif packet[IP].proto == 41: return IPv6 return False def wait_for_netlink_msgs(self): """ Gathers any RTM_NEWLINK messages Returns: (list) containing any received messages """ msgs = [] with IPRoute() as ipr: ipr.bind() for msg in ipr.get(): if msg['event'] == RTM_NEWLINK: msgs.append(msg) return msgs def sniffer_restart_required(self, messages): """ Determines if the packet sniffer needs to be restarted A restart is required if all of the following conditions are met: 1. A netlink message of type RTM_NEWLINK is received (this is checked by `wait_for_netlink_msgs`) 2. The interface index of the message corresponds to a portchannel interface 3. The state of the interface in the message is 'up' Here, we do not care about an interface going down since the sniffer is able to continue sniffing on the other interfaces. However, if an interface has gone down and come back up, we need to restart the sniffer to be able to sniff traffic on the interface that has come back up. """ for msg in messages: if self.netlink_msg_is_for_portchannel(msg): if msg['state'] == 'up': logger.log_info('{} came back up, sniffer restart required' .format(self.get_intf_name(msg))) return True return False def start_sniffer(self): """ Starts an AsyncSniffer and waits for it to inititalize fully """ self.sniffer = AsyncSniffer( iface=self.sniff_intfs, filter=self.packet_filter, prn=self.ping_inner_dst, store=0 ) self.sniffer.start() while not hasattr(self.sniffer, 'stop_cb'): time.sleep(0.1) def ping_inner_dst(self, packet): """ Pings the inner destination IP for an encapsulated packet Args: packet: The encapsulated packet received """ inner_packet_type = self.get_inner_pkt_type(packet) if inner_packet_type and packet[IP].dst == self.self_ip: cmds = ['timeout', '0.2', 'ping', '-c1', '-W1', '-i0', '-n', '-q'] if inner_packet_type == IPv6: cmds.append('-6') dst_ip = packet[IP].payload[inner_packet_type].dst cmds.append(dst_ip) logger.log_info("Running command '{}'".format(' '.join(cmds))) subprocess.run(cmds, stdout=subprocess.DEVNULL) def listen_for_tunnel_pkts(self): """ Listens for tunnel packets that are trapped to CPU These packets may be trapped if there is no neighbor info for the inner packet destination IP in the hardware. """ self.self_ip, peer_ip = self.get_ipinip_tunnel_addrs() if self.self_ip is None or peer_ip is None: logger.log_notice('Could not get tunnel addresses from ' 'config DB, exiting...') return None self.packet_filter = 'host {} and host {}'.format(self.self_ip, peer_ip) logger.log_notice('Starting tunnel packet handler for {}' .format(self.packet_filter)) self.sniff_intfs = self.get_up_portchannels() logger.log_info("Listening on interfaces {}".format(self.sniff_intfs)) self.start_sniffer() while True: msgs = self.wait_for_netlink_msgs() if self.sniffer_restart_required(msgs): self.sniffer.stop() sniff_intfs = self.get_up_portchannels() logger.log_notice('Restarting tunnel packet handler on ' 'interfaces {}'.format(sniff_intfs)) self.start_sniffer() def run(self): """ Entry point for the TunnelPacketHandler class """ self.wait_for_portchannels() self.listen_for_tunnel_pkts()