Source code for switchyard.switchy_real

import sys
import argparse
import os
import signal
import re
import subprocess
import time
import threading
import textwrap
from queue import Queue,Empty
from socket import gethostname

from switchyard.lib.address import *
from switchyard.lib.packet import *
from switchyard.lib.pcapffi import *
from switchyard.lib.importcode import import_or_die

_dlt_to_decoder = {}
_dlt_to_decoder[Dlt.DLT_EN10MB] = lambda raw: Packet(raw, first_header=Ethernet)
_dlt_to_decoder[Dlt.DLT_NULL] = lambda raw: Packet(raw, first_header=Null)

'''
Low-level-ish packet library for PyRouter project.  Uses a FFI-based
pcap bridge library (pcapffi) for receiving and sending packets, 
and the Switchyard packet library for packet parsing.

jsommers@colgate.edu
'''

USERMAIN = 'switchy_main'

from switchyard.lib.common import Interface, SwitchyException, Shutdown, NoPackets, LLNetBase
from switchyard.lib.common import setup_logging, log_info, log_debug, log_warn, log_failure
from switchyard.lib.textcolor import *


[docs]class PyLLNet(LLNetBase): ''' A class that represents a collection of network devices on which packets can be received and sent. ''' def __init__(self, devlist, name=None): LLNetBase.__init__(self) signal.signal(signal.SIGINT, self._sig_handler) signal.signal(signal.SIGTERM, self._sig_handler) signal.signal(signal.SIGHUP, self._sig_handler) signal.signal(signal.SIGUSR1, self._sig_handler) signal.signal(signal.SIGUSR2, self._sig_handler) self.devs = devlist # self.__initialize_devices(includelist, excludelist) self.devinfo = self.__assemble_devinfo() self.pcaps = {} self.__make_pcaps() log_info("Using network devices: {}".format(' '.join(self.devs))) for devname, intf in self.devinfo.items(): log_debug("{}: {}".format(devname, str(intf))) PyLLNet.running = True self.__spawn_threads() if name: self.__name = name else: self.__name = gethostname() @property def name(self): return self.__name def __initialize_devices(self, includes, excludes): devs = self.__get_net_devs() if not devs: raise SwitchyException("No suitable interfaces found.") # remove devs from excludelist devs.difference_update(set(excludes)) # if includelist is non-empty, perform # intersection with devs found and includelist if includes: devs.intersection_update(set(includes)) if not devs: raise SwitchyException("No interfaces enabled after handling include/exclude lists") return devs
[docs] def shutdown(self): ''' Should be called by Switchyard user code when a network object is being shut down. (This method cleans up internal threads and network interaction objects.) ''' if not PyLLNet.running: return PyLLNet.running = False log_debug("Joining threads for shutdown") for t in self.threads: t.join() log_debug("Closing pcap devices") for devname,pdev in self.pcaps.items(): pdev.close() log_debug("Done cleaning up")
def __spawn_threads(self): ''' Internal method. Creates threads to handle low-level network receive. ''' self.threads = [] self.pktqueue = Queue() for devname,pdev in self.pcaps.items(): t = threading.Thread(target=PyLLNet.__low_level_dispatch, args=(pdev, devname, self.pktqueue)) t.start() self.threads.append(t) def __assemble_devinfo(self): ''' Internal method. Assemble information on each interface/ device that we know about, i.e., its MAC address and configured IP address and prefix. ''' devinfo = {} # beautiful/ugly regular expressions hwaddr = re.compile("HWaddr ([0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2})") ether = re.compile("ether ([0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2})") ipmasklinux = re.compile("inet addr:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+Bcast:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+Mask:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})") ipmaskosx = re.compile("inet (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+netmask (0x[0-9a-f]{8})") # FIXME: ip6 for devname in self.devs: macaddr = None ipaddr = None mask = None output = subprocess.check_output(["ifconfig", devname]) for line in output.decode('ascii','').split('\n'): mobj = hwaddr.search(line) if mobj: macaddr = EthAddr(mobj.groups()[0]) else: mobj = ether.search(line) if mobj: macaddr = EthAddr(mobj.groups()[0]) mobj = ipmasklinux.search(line) if mobj: ipaddr = IPAddr(mobj.groups()[0]) mask = IPAddr(mobj.groups()[2]) else: mobj = ipmaskosx.search(line) if mobj: ipaddr = IPAddr(mobj.groups()[0]) mask = IPAddr(int(mobj.groups()[1], base=16)) devinfo[devname] = Interface(devname, macaddr, ipaddr, mask) return devinfo def __make_pcaps(self): ''' Internal method. Create libpcap devices for every network interface we care about and set them in non-blocking mode. ''' self.pcaps = {} for dev in self.devs: thismac = self.devinfo[dev].ethaddr pdev = PcapLiveDevice(dev) # default snaplen is 64k # pcap FIXME # pdev.setfilter("not ether src {}".format(thismac)) self.pcaps[dev] = pdev def _sig_handler(self, signum, stack): ''' Handle process INT signal. ''' log_debug("Got SIGINT.") if signum == signal.SIGINT: PyLLNet.running = False if self.pktqueue.qsize() == 0: # put dummy pkt in queue to unblock a # possibly stuck user thread self.pktqueue.put( (None,None,None) ) self.pktqueue = Queue() @staticmethod def __low_level_dispatch(pcapdev, devname, pktqueue): ''' Thread entrypoint for doing low-level receive and dispatch for a single pcap device. ''' count = 0 while PyLLNet.running: # a non-zero timeout value is ok here; this is an # independent thread that handles input for this # one pcap device. it throws any packets received # into the shared queue (which is read by the actual # user code) pktinfo = pcapdev.recv_packet(timeout=0.2) if pktinfo is None: continue log_debug("Got packet on device {}, dlt {}".format(devname, pcapdev.dlt)) pktqueue.put( (devname,pcapdev.dlt,pktinfo) ) count += 1 if count % 100 == 0: stats = pcapdev.stats() log_debug("Periodic device statistics {}: {} received, {} dropped, {} dropped/if".format(devname, stats.ps_recv, stats.ps_drop, stats.ps_ifdrop)) log_debug("Receiver thread for {} exiting".format(devname)) stats = pcapdev.stats() log_debug("Final device statistics {}: {} received, {} dropped, {} dropped/if".format(devname, stats.ps_recv, stats.ps_drop, stats.ps_ifdrop))
[docs] def recv_packet(self, timeout=None, timestamp=False): ''' Receive packets from any device on which one is available. Blocks until it receives a packet, unless a timeout value >=0 is given. Raises Shutdown exception when device(s) are shut down (i.e., on a SIGINT to the process). Raises NoPackets when there are no packets that can be read. Returns a tuple of length 2 or 3, depending on whether the timestamp is desired. * device: network device name on which packet was received as a string * timestamp: floating point value of time at which packet was received (optionally returned; only if timestamp=True) * packet: Switchyard Packet object. ''' while True: try: dev,dlt,pktinfo = self.pktqueue.get(timeout=timeout) if not PyLLNet.running: break decoder = _dlt_to_decoder.get(dlt, None) if decoder is None: log_warn("Received packet with unparseable encapsulation {}".format(dlt)) continue pkt = decoder(pktinfo.raw) if timestamp: return dev,pktinfo.timestamp,pkt else: return dev,pkt except Empty: raise NoPackets() raise Shutdown()
[docs] def send_packet(self, dev, packet): ''' Send a Switchyard Packet object on the given device (string name of device). Raises SwitchyException if packet object isn't valid, or device name isn't recognized. ''' pdev = self.pcaps.get(dev, None) if not pdev: raise SwitchyException("Unrecognized device name for packet send: {}".format(dev)) else: if packet is None: raise SwitchyException("No packet object given to send_packet") if not isinstance(packet, Packet): raise SwitchyException("Object given to send_packet is not a Packet (it is: {})".format(type(packet))) # convert packet to bytes and send it rawpkt = packet.to_bytes() log_debug("Sending packet on device {}: {}".format(dev, str(packet))) pdev.send_packet(rawpkt)
def main_real(usercode, netobj, options): ''' Entrypoint function for non-test ("real") mode. At this point we assume that we are running as root and have pcap module. ''' usercode_entry_point = import_or_die(usercode, ('main','srpy_main','switchy_main')) if options.dryrun: log_info("Imported your code successfully. Exiting dry run.") netobj.shutdown() return try: usercode_entry_point(netobj) except Exception as e: import traceback log_failure("Exception while running your code: {}".format(e)) message = '''{0} This is the Switchyard equivalent of the blue screen of death. Here (repeating what's above) is the failure that occurred: '''.format('*'*60, textwrap.fill(str(e), 60)) with red(): print(message) traceback.print_exc(1) print('*'*60) if options.nohandle: raise if not options.nopdb: print(''' I'm throwing you into the Python debugger (pdb) at the point of failure. If you don't want pdb, use the --nopdb flag to avoid this fate. ''') import pdb pdb.post_mortem() else: netobj.shutdown()