commit 9a37859159c0922eb7155ab0a109206af5466062 Author: Isis Lovecruft isis@torproject.org Date: Wed Oct 31 16:52:36 2012 +0000
* Updated echo test to the new API. Need to merge master and retest. --- ooni/echo.py | 279 +++++++++++++++++++++++++++++++++++++--------------------- 1 files changed, 178 insertions(+), 101 deletions(-)
diff --git a/ooni/echo.py b/ooni/echo.py index 1417622..c5f4f47 100644 --- a/ooni/echo.py +++ b/ooni/echo.py @@ -1,10 +1,10 @@ #!/usr/bin/env python -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- # # +---------+ # | echo.py | # +---------+ -# A simply ICMP-8 ping test. +# A simple ICMP-8 ping test. # # :author: Isis Lovecruft # :version: 0.0.1-pre-alpha @@ -15,115 +15,192 @@ import os import sys
-from twisted.plugin import IPlugin -from twisted.python import usage -from zope.interface import implements +from pprint import pprint + +from twisted.internet import reactor +from twisted.plugin import IPlugin +from twisted.python import usage +from ooni.nettest import TestCase +from ooni.utils import log, Storage +from ooni.utils.net import PermissionsError, IfaceError + +try: + from scapy.all import sr1, IP, ICMP ## XXX v4/v6? + from ooni.lib import txscapy + from ooni.lib.txscapy import txsr, txsend + from ooni.templates.scapyt import ScapyTest +except: + log.msg("This test requires scapy, see www.secdev.org/projects/scapy") + +## xxx TODO: move these to a utility function for determining OSes +LINUX=sys.platform.startswith("linux") +OPENBSD=sys.platform.startswith("openbsd") +FREEBSD=sys.platform.startswith("freebsd") +NETBSD=sys.platform.startswith("netbsd") +DARWIN=sys.platform.startswith("darwin") +SOLARIS=sys.platform.startswith("sunos") +WINDOWS=sys.platform.startswith("win32")
-from ooni.lib import txscapy -from ooni.utils import log -from ooni.plugoo.assets import Asset -from ooni.plugoo.interface import ITest -from ooni.protocols.scapyproto import ScapyTest - -from ooni import nettest +class EchoTest(ScapyTest): + """ + xxx fill me in + """ + name = 'echo' + author = 'Isis Lovecruft isis@torproject.org' + description = 'A simple ICMP-8 test to see if a host is reachable.' + version = '0.0.1' + inputFile = ['file', 'f', None, 'File of list of IPs to ping'] + requirements = None + report = Storage()
-class EchoOptions(nettest.): optParameters = [ ['interface', 'i', None, 'Network interface to use'], - ['destination', 'd', None, 'File of hosts to ping'], ['count', 'c', 5, 'Number of packets to send', int], ['size', 's', 56, 'Number of bytes to send in ICMP data field', int], - ['ttl', 't', 25, 'Set the IP Time to Live', int], + ['ttl', 'l', 25, 'Set the IP Time to Live', int], + ['timeout', 't', 2, 'Seconds until timeout if no response', int], + ['pcap', 'p', None, 'Save pcap to this file'] ] - optFlags = [] - -class EchoAsset(Asset): - def __init__(self, file=None): - self = Asset.__init__(self, file) - - def parse_line(self, line): - if line.startswith('#'): - return + optFlags = [['receive', 'r', True, 'Receive response packets']] + + def setUpClass(self, *a, **kw): + ''' + :ivar ifaces: + Struct returned from getifaddrs(3) and turned into a tuple in the + form (*ifa_name, AF_FAMILY, *ifa_addr) + ''' + super(EchoTest, self).__init__(*a, **kw) + + ## allow subclasses which register/implement external classes + ## to define their own reactor without overrides: + if not hasattr(super(EchoTest, self), 'reactor'): + log.debug("%s test: Didn't find reactor!" % self.name) + self.reactor = reactor + + if self.localOptions: + log.debug("%s localOptions found" % self.name) + log.debug("%s test options: %s" % (self.name, self.subOptions)) + self.local_options = self.localOptions.parseOptions(self.subOptions) + for key, value in self.local_options: + log.debug("Set attribute %s[%s] = %s" % (self.name, key, value)) + setattr(self, key, value) + + ## xxx is this now .subOptions? + #self.inputFile = self.localOptions['file'] + self.timeout *= 1000 ## convert to milliseconds + + if not self.interface: + log.msg("No network interface specified!") + log.debug("OS detected: %s" % sys.platform) + if LINUX or OPENBSD or NETBSD or FREEBSD or DARWIN or SOLARIS: + from twisted.internet.test import _posixifaces + log.msg("Attempting to discover network interfaces...") + ifaces = _posixifaces._interfaces() + elif WINDOWS: + from twisted.internet.test import _win32ifaces + log.msg("Attempting to discover network interfaces...") + ifaces = _win32ifaces._interfaces() + else: + log.debug("Client OS %s not accounted for!" % sys.platform) + log.debug("Unable to discover network interfaces...") + ifaces = [('lo', '')] + + ## found = {'eth0': '1.1.1.1'} + found = [{i[0]: i[2]} for i in ifaces if i[0] != 'lo'] + log.info("Found interfaces:\n%s" % pprint(found)) + self.interfaces = self.tryInterfaces(found) else: - return line.replace('\n', '') - -class EchoTest(ScapyTest): - implements(IPlugin, ITest) - - shortName = 'echo' - description = 'A simple ICMP-8 test to check if a host is reachable' - options = EchoOptions - requirements = None - blocking = False - - pcap_file = 'echo.pcap' - receive = True - - def initialize(self): - self.request = {} - self.response = {} - - if self.local_options: - - options = self.local_options - - if options['interface']: - self.interface = options['interface'] - - if options['count']: - ## there's a Counter() somewhere, use it - self.count = options['count'] - - if options['size']: - self.size = options['size'] - - if options['ttl']: - self.ttl = options['ttl'] - - def load_assets(self): - assets = {} - option = self.local_options - - if option and option['destination']: + ## xxx need a way to check that iface exists, is up, and + ## we have permissions on it + log.debug("Our interface has been set to %s" % self.interface)
+ if self.pcap: try: - from scapy.all import IP + self.pcapfile = open(self.pcap, 'a+') except: - log.err() - - if os.path.isfile(option['destination']): - with open(option['destination']) as hosts: - for line in hosts.readlines(): - assets.update({'host': EchoAsset(line)}) - else: - while type(options['destination']) is str: - try: - IP(options['destination']) - except: - log.err() - break - assets.update({'host': options['destination']}) + log.msg("Unable to write to pcap file %s" % self.pcap) + self.pcapfile = None + + try: + assert os.path.isfile(self.file) + fp = open(self.file, 'r') + except Exception, e: + hosts = ['8.8.8.8', '38.229.72.14'] + log.err(e) + else: + self.inputs = self.inputProcessor(fp) + self.removePorts(hosts) + + log.debug("Initialization of %s test completed with:\n%s" + % (self.name, ''.join(self.__dict__))) + + def inputProcessor(self, fp): + inputs = [x.strip() for x in fp.readlines() if not x.startswith('#')] + fp.close() + log.debug("Removing possible ports from host addresses...") + log.debug("Initial inputs:\n%s" % pprint(inputs)) + + assert isinstance(inputs, list) + hosts = [h.rsplit(':', 1)[0] for h in inputs] + log.debug("Inputs converted to:\n%s" % hosts) + + return hosts + + def tryInterfaces(self, ifaces): + try: + from scapy.all import sr1 ## we want this check to be blocking + except: + log.msg("This test requires scapy: www.secdev.org/projects/scapy") + raise SystemExit + + ifup = {} + while ifaces: + for ifname, ifaddr in ifaces: + log.debug("Currently testing network capabilities of interface" + + "%s by sending a packet to our address %s" + % (ifname, ifaddr)) + try: + pkt = IP(dst=ifaddr)/ICMP() + ans, unans = sr(pkt, iface=ifname, timeout=self.timeout) + except Exception, e: + raise PermissionsError if e.find("Errno 1") else log.err(e) else: - log.msg("Couldn't understand destination option...") - log.msg("Give one IPv4 address, or a file with one address per line.") - return assets - - def experiment(self, args): - if len(args) == 0: - log.err("Error: We're Echo, not Narcissus!") - log.err(" Provide a list of hosts to ping...") - d = sys.exit(1) - return d - - ## XXX v4 / v6 - from scapy.all import ICMP, IP, sr - ping = sr(IP(dst=args)/ICMP()) - if ping: - self.response.update(ping.show()) + ## xxx i think this logic might be wrong + log.debug("Interface test packet\n%s\n\n%s" + % (pkt.summary(), pkt.show2())) + if ans.summary(): + log.info("Received answer for test packet on interface" + +"%s :\n%s" % (ifname, ans.summary())) + ifup.update(ifname, ifaddr) + else: + log.info("Our interface test packet was unanswered:\n%s" + % unans.summary()) + + if len(ifup) > 0: + log.msg("Discovered the following working network interfaces: %s" + % ifup) + return ifup else: - log.msg('No response received from %s' % args) - - def control(self, *args): - pass - -echo = EchoTest(None, None, None) + raise IfaceError("Could not find a working network interface.") + + def buildPackets(self): + log.debug("self.input is %s" % self.input) + log.debug("self.hosts is %s" % self.hosts) + for addr in self.input: + packet = IP(dst=self.input)/ICMP() + self.request.append(packet) + return packet + + def test_icmp(self): + if self.recieve: + self.buildPackets() + all = [] + for packet in self.request: + d = self.sendReceivePackets(packets=packet) + all.append(d) + self.response.update({packet: d}) + d_list = defer.DeferredList(all) + return d_list + else: + d = self.sendPackets() + return d