commit 8af6d133c5f48ae516842674602a5198c9dcece8 Author: Isis Lovecruft isis@torproject.org Date: Fri Nov 16 15:20:49 2012 +0000
Re-ported echo. Fixed parameters not being passed through txscapy.sr to parent methods. --- nettests/bridge_reachability/echo.py | 141 ++++++++++++++++++++++++--------- ooni/utils/txscapy.py | 12 ++- 2 files changed, 110 insertions(+), 43 deletions(-)
diff --git a/nettests/bridge_reachability/echo.py b/nettests/bridge_reachability/echo.py index df121b4..06c3aa4 100644 --- a/nettests/bridge_reachability/echo.py +++ b/nettests/bridge_reachability/echo.py @@ -16,34 +16,39 @@ import os import sys
from twisted.python import usage -from twisted.internet import reactor, defer -from ooni.nettest import NetTestCase +from twisted.internet import reactor, defer, address +from ooni import nettest from ooni.utils import log, net, Storage
try: - from scapy.all import IP, ICMP - from scapy.all import sr1 - from ooni.lib import txscapy - from ooni.lib.txscapy import txsr, txsend - from ooni.templates.scapyt import BaseScapyTest -except: + from scapy.all import IP, ICMP + from scapy.all import sr1 + from ooni.utils import txscapy +except Exception, e: log.msg("This test requires scapy, see www.secdev.org/projects/scapy") + log.exception(e)
class UsageOptions(usage.Options): + """ + Options for EchoTest. + + Note: 'count', 'size', and 'ttl' have yet to be implemented. + """ optParameters = [ ['dst', 'd', None, 'Host IP to ping'], ['file', 'f', None, 'File of list of IPs to ping'], + ['pcap', 'p', None, 'Save pcap to this file'], ['interface', 'i', None, 'Network interface to use'], + ['receive', 'r', True, 'Receive response packets'], + ['timeout', 't', 2, 'Seconds to wait if no response', int], ['count', 'c', 1, 'Number of packets to send', int], - ['size', 's', 56, 'Number of bytes to send in ICMP data field', 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'], - ['receive', 'r', True, 'Receive response packets']] + ['size', 's', 56, 'Bytes to send in ICMP data field', int], + ['ttl', 'l', 25, 'Set the IP Time to Live', int]]
-class EchoTest(BaseScapyTest): +class EchoTest(nettest.NetTestCase): """ Basic ping test. This takes an input file containing one IP or hostname + per line. """ name = 'echo' author = 'Isis Lovecruft isis@torproject.org' @@ -55,6 +60,22 @@ class EchoTest(BaseScapyTest): #requiredOptions = ['dst']
def setUp(self, *a, **kw): + """ + Send an ICMP-8 packet to a host IP, and process the response. + + @param timeout: + Seconds after sending the last packet to timeout. + @param interface: + The interface to restrict listening to. + @param dst: + A single host to ping. + @param file: + A file of hosts to ping, one per line. + @param receive: + Whether or not to receive replies. Defaults to True. + @param pcap: + The file to save packet captures to. + """ self.destinations = {}
if self.localOptions: @@ -87,11 +108,10 @@ class EchoTest(BaseScapyTest): self.dstProcessor(self.file) for address, details in self.destinations.items(): for labels, data in details.items(): - if not 'ans' in labels: + if not 'response' in labels: self.dst = details['dst_ip'] else: self.addDest(self.dst) - log.debug("self.dst is now: %s" % self.dst)
log.debug("Initialization of %s test completed." % self.name)
@@ -107,31 +127,74 @@ class EchoTest(BaseScapyTest): continue self.addDest(line)
- def test_icmp(self): + def build_packets(self): + """ + Construct a list of packets to send out. + """ + packets = [] + for dest, data in self.destinations.items(): + pkt = IP(dst=dest)/ICMP() + packets.append(pkt) + ## XXX if a domain was specified, we need a way to check that + ## its IP matches the one we're seeing in pkt.src + #try: + # address.IPAddress(dest) + #except: + # data['dst_ip'] = pkt.dst + return packets
- def process_response(pkt, dest): - try: - ans, unans = pkt - if ans: - log.msg("Recieved echo-reply: %s" % pkt.summary()) - self.destinations[dest]['ans'] = a.show2() - self.report['response'] = [a.show2() for a in ans] - self.report['censored'] = False - else: - log.msg("No reply from %s. Possible censorship event." % dest) - log.debug("Unanswered packets: %s" % unans.summary()) - self.report['response'] = [u.show2() for u in unans] - self.report['censored'] = True - except Exception, e: - log.exception(e) + def test_icmp(self): + """ + Send the list of ICMP packets.
+ TODO: add end summary progress report for % answered, etc. + """ try: - for dest, data in self.destinations.items(): - reply = txsr(IP(dst=dest)/ICMP(), - iface=self.interface, - retry=self.count, - multi=True, - timeout=self.timeout) - process = process_response(reply, dest) + def nicely(packets): + """Print scapy summary nicely.""" + return list([x.summary() for x in packets]) + + def process_answered((answered, sent)): + """Callback function for txscapy.sr().""" + self.report['sent'] = nicely(sent) + self.report['answered'] = [nicely(ans) for ans in answered] + + for req, resp in answered: + log.msg("Received echo-reply:\n%s" % resp.summary()) + for dest, data in self.destinations.items(): + if data['dst_ip'] == resp.src: + data['response'] = resp.summary() + data['censored'] = False + for snd in sent: + if snd.dst == resp.src: + answered.remove((req, resp)) + return (answered, sent) + + def process_unanswered((unanswered, sent)): + """ + Callback function for remaining packets and destinations which + do not have an associated response. + """ + if len(unanswered) > 0: + nicer = [nicely(unans) for unans in unanswered] + log.msg("Unanswered/remaining packets:\n%s" + % nicer) + self.report['unanswered'] = nicer + for dest, data in self.destinations.items(): + if not 'response' in data: + log.msg("No reply from %s. Possible censorship event." + % dest) + data['response'] = None + data['censored'] = True + return (unanswered, sent) + + packets = self.build_packets() + d = txscapy.sr(packets, iface=self.interface, multi=True) + d.addCallback(process_answered) + d.addErrback(log.exception) + d.addCallback(process_unanswered) + d.addErrback(log.exception) + self.report['destinations'] = self.destinations + return d except Exception, e: log.exception(e) diff --git a/ooni/utils/txscapy.py b/ooni/utils/txscapy.py index 2b108ca..9c33d18 100644 --- a/ooni/utils/txscapy.py +++ b/ooni/utils/txscapy.py @@ -1,4 +1,5 @@ -# -*- coding:utf8 -*- +# -*- coding: utf-8 -*- +# # txscapy # ******* # Here shall go functions related to using scapy with twisted. @@ -32,7 +33,7 @@ class TXPcapWriter(PcapWriter):
class ScapyProtocol(abstract.FileDescriptor): def __init__(self, super_socket=None, - reactor=None, timeout=None, receive=True): + reactor=None, timeout=None, receive=True, *a, **kw): abstract.FileDescriptor.__init__(self, reactor) # By default we use the conf.L3socket if not super_socket: @@ -54,7 +55,10 @@ class ScapyProtocol(abstract.FileDescriptor): # This deferred will fire when we have finished sending a receiving packets. self.d = defer.Deferred() self.debug = False + self.multi = False + if kw['multi']: + self.multi = kw['multi'] # XXX this needs to be implemented. It would involve keeping track of # the state of the sending via the super socket file descriptor and # firing the callback when we have concluded sending. Check out @@ -127,9 +131,9 @@ class ScapyProtocol(abstract.FileDescriptor): self.sendPackets(packets) return self.d
-def sr(x, filter=None, iface=None, nofilter=0, timeout=None): +def sr(x, filter=None, iface=None, nofilter=0, timeout=None, *a, **kw): super_socket = conf.L3socket(filter=filter, iface=iface, nofilter=nofilter) - sp = ScapyProtocol(super_socket=super_socket, timeout=timeout) + sp = ScapyProtocol(super_socket=super_socket, timeout=timeout, *a, **kw) return sp.startSending(x)
def send(x, filter=None, iface=None, nofilter=0, timeout=None):