[tor-commits] [ooni-probe/master] Added better reporting of statistics and safe packet information to TCPflags

isis at torproject.org isis at torproject.org
Tue Dec 18 05:53:46 UTC 2012


commit 1fa46bcb6d65a11b3209c07d8e14507d43303f1a
Author: Isis Lovecruft <isis at torproject.org>
Date:   Tue Nov 27 14:07:52 2012 +0000

    Added better reporting of statistics and safe packet information to TCPflags
    test, which is a combined and condensed version of TCPsyn and TCPfin tests.
    
    * Tested pcap support.
    * Updated documentation.
---
 nettests/bridge_reachability/tcpsyn.py |  199 ++++++++++++++++++++------------
 1 files changed, 124 insertions(+), 75 deletions(-)

diff --git a/nettests/bridge_reachability/tcpsyn.py b/nettests/bridge_reachability/tcpsyn.py
index 54271bf..555fdef 100644
--- a/nettests/bridge_reachability/tcpsyn.py
+++ b/nettests/bridge_reachability/tcpsyn.py
@@ -16,12 +16,13 @@
 import os
 import sys
 
-from ipaddr           import IPAddress
-from twisted.python   import usage
-from twisted.internet import reactor, defer, address
-from ooni             import nettest
-from ooni.utils       import net, log
-from ooni.utils.otime import timestamp
+from ipaddr                 import IPAddress
+from twisted.python         import usage
+from twisted.python.failure import Failure
+from twisted.internet       import reactor, defer, address
+from ooni                   import nettest, config
+from ooni.utils             import net, log
+from ooni.utils.otime       import timestamp
 
 try:
     from scapy.all          import TCP, IP
@@ -30,8 +31,8 @@ except:
     log.msg("This test requires scapy, see www.secdev.org/projects/scapy")
 
 
-class UsageOptions(usage.Options):
-    """Options for TCPSynTest."""
+class TCPFlagOptions(usage.Options):
+    """Options for TCPTest."""
     optParameters = [
         ['dst', 'd', None, 'Host IP to ping'],
         ['port', 'p', None, 'Host port'],
@@ -43,7 +44,7 @@ class UsageOptions(usage.Options):
         ['cerealize', 'z', False,
          'Cerealize scapy objects for further scripting']]
 
-class TCPSynTest(nettest.NetTestCase):
+class TCPFlagTest(nettest.NetTestCase):
     """
     Sends only a TCP SYN packet to a host IP:PORT, and waits for either a
     SYN/ACK, a RST, or an ICMP error.
@@ -51,37 +52,55 @@ class TCPSynTest(nettest.NetTestCase):
     TCPSynTest can take an input file containing one IP:Port pair per line, or
     the commandline switches --dst <IP> and --port <PORT> can be used.
     """
-    name         = 'TCP SYN'
+    name         = 'TCP Flag'
     author       = 'Isis Lovecruft <isis at torproject.org>'
-    description  = 'A TCP SYN test to see if a host is reachable.'
+    description  = 'A TCP SYN/ACK/FIN test to see if a host is reachable.'
     version      = '0.0.1'
     requiresRoot = True
 
-    usageOptions = UsageOptions
+    usageOptions = TCPFlagOptions
     inputFile    = ['file', 'f', None, 'File of list of IP:PORTs to ping']
 
-    destinations = {}
+    #destinations = {}
 
     def setUp(self, *a, **kw):
         """Configure commandline parameters for TCPSynTest."""
+        self.report = {}
+
         if self.localOptions:
             for key, value in self.localOptions.items():
                 setattr(self, key, value)
         if not self.interface:
             try:
-                iface = log.catcher(net.getDefaultIface())
+                iface = log.catch(net.getDefaultIface())
             except net.IfaceError, ie:
                 log.warn("Could not find a working network interface!")
+                log.fail(ie)
             else:
                 log.msg("Using system default interface: %s" % iface)
                 self.interface = iface
+        if config.debug:
+            defer.setDebugging(on)
+
+    def addToDestinations(self, addr='0.0.0.0', port='443'):
+        """
+        Validate and add an IP address and port to the dictionary of
+        destinations to send to. If the host's IP is already in the
+        destinations, then only add the port.
 
-    def addToDestinations(self, addr, port):
+        @param addr: A string representing an IPv4 or IPv6 address.
+        @param port: A string representing a port number.
+        @returns: A 2-tuple containing the address and port.
+        """
         dst, dport = net.checkIPandPort(addr, port)
-        if not dst in self.destinations.keys():
-            self.destinations[dst] = {'dst': dst, 'dport': dport}
+        #if not dst in self.destinations.keys():
+        if not dst in self.report.keys():
+            #self.destinations[dst] = {'dst': dst, 'dport': [dport]}
+            self.report[dst] = {'dst': dst, 'dport': [dport]}
         else:
-            log.debug("XXX Multiple port scanning not yet implemented.")
+            log.debug("Got additional port for destination.")
+            #self.destinations[dst]['dport'].append(dport)
+            self.report[dst]['dport'].append(dport)
         return (dst, dport)
 
     def inputProcessor(self, input_file=None):
@@ -105,6 +124,12 @@ class TCPSynTest(nettest.NetTestCase):
                     raw_ip, raw_port = one.rsplit(':', 1) ## XXX not ipv6 safe!
                     yield self.addToDestinations(raw_ip, raw_port)
 
+    @log.catch
+    def createPDF(self, results):
+        pdfname = self.name + '_' + timestamp()
+        results.pdfdump(pdfname)
+        log.msg("Visual packet conversation saved to %s.pdf" % pdfname)
+
     @staticmethod
     def build_packets(addr, port, flags=None, count=3):
         """Construct a list of packets to send out."""
@@ -113,64 +138,88 @@ class TCPSynTest(nettest.NetTestCase):
             packets.append( IP(dst=addr)/TCP(dport=port, flags=flags) )
         return packets
 
-    @log.catcher
-    def test_tcp_syn(self):
-        """Send the list of SYN packets."""
-
-        def process_packets(packet_list):
-            """xxx"""
-            results, unanswered = packet_list
-
-            if self.pdf:
-                pdf_name = self.name  +'_'+ timestamp()
-                try:
-                    results.pdfdump(pdf_name)
-                except Exception, ex:
-                    log.exception(ex)
-                else:
-                    log.msg("Visual packet conversation saved to %s.pdf"
-                            % pdf_name)
-
-            for (q, r) in results:
-                request_data = {'dst': q.dst,
-                                'dport': q.dport,
-                                'summary': q.summary(),
-                                'command': q.command(),
-                                'sent_time': q.time}
-                response_data = {'summary': r.summary(),
-                                 'command': r.command(),
-                                 'src': r['IP'].src,
-                                 'flags': r['IP'].flags,
-                                 'recv_time': r.time,
-                                 'delay': r.time - q.time}
-                if self.hexdump:
-                    request_data.update('hexdump', q.hexdump())
-                    response_data.update('hexdump', r.hexdump())
-                for dest, data in self.destinations.items():
-                    if data['dst'] == response_data['src']:
-                        if not 'reachable' in data:
-                            if self.hexdump:
-                                log.msg("%s\n%s" % (q.hexdump(), r.hexdump()))
-                            else:
-                                log.msg(" Received response:\n%s ==> %s"
-                                        % (q.mysummary(), r.mysummary()))
-                            data.update( {'reachable': True,
-                                          'request': request_data,
-                                          'response': response_data} )
-            return unanswered
-
-        def process_unanswered(unanswered):
-            """Callback function to process unanswered packets."""
-            if unanswered is not None and len(unanswered) > 0:
-                log.msg("Waiting on responses from\n%s" %
-                        '\n'.join([unans.summary() for unans in unanswered]))
-            log.msg("Writing response packet information to report...")
-            self.report = (self.destinations)
-            return self.destinations
+    @staticmethod
+    def process_packets(packet_list):
+        """
+        If the source address of packet in :param:packet_list matches one of our input
+        destinations, then extract some of the information from it to the test report.
+
+        @param packet_list:
+            A :class:scapy.plist.PacketList
+        """
+        results, unanswered = packet_list
+
+        if self.pdf:
+            self.createPDF(results)
+
+        for (q, r) in results:
+            request_data = {'dst': q.dst,
+                            'dport': q.dport,
+                            'summary': q.summary(),
+                            'command': q.command(),
+                            'sent_time': q.time}
+            response_data = {'src': r['IP'].src,
+                             'flags': r['IP'].flags,
+                             'summary': r.summary(),
+                             'command': r.command(),
+                             'recv_time': r.time,
+                             'delay': r.time - q.time}
+            if self.hexdump:
+                request_data.update('hexdump', q.hexdump())
+                response_data.update('hexdump', r.hexdump())
+            for dest, data in self.destinations.items():
+                if data['dst'] == response_data['src']:
+                    if not 'reachable' in data:
+                        if self.hexdump:
+                            log.msg("%s\n%s" % (q.hexdump(), r.hexdump()))
+                        else:
+                            log.msg(" Received response:\n%s ==> %s"
+                                    % (q.mysummary(), r.mysummary()))
+                        data.update( {'reachable': True,
+                                      'request': request_data,
+                                      'response': response_data} )
+        return unanswered
 
+    @staticmethod
+    def process_unanswered(unanswered):
+        """Callback function to process unanswered packets."""
+        if unanswered is not None and len(unanswered) > 0:
+            log.msg("Waiting on responses from\n%s" %
+                    '\n'.join( [unans.summary() for unans in unanswered] ))
+        log.msg("Writing response packet information to report...")
+        self.report = (self.destinations)
+        return self.destinations
+
+    @log.catch
+    def tcp_flags(self, flags="S"):
+        """
+        Generate, send, and listen for responses to, a list of TCP/IP packets
+        to an address and port pair taken from the current input, and a string
+        specifying the TCP flags to set.
+
+        @param flags:
+            A string representing the TCP flags to be set, i.e. "SA" or "F".
+            Defaults to "S".
+        """
         (addr, port) = self.input
-        packets = self.build_packets(addr, port, "S", self.count)
+        packets = self.build_packets(addr, port, str(flags), self.count)
         d = txscapy.sr(packets, iface=self.interface)
-        d.addCallbacks(process_packets, log.exception)
-        d.addCallbacks(process_unanswered, log.exception)
+        d.addCallbacks(self.process_packets, log.exception)
+        d.addCallbacks(self.process_unanswered, log.exception)
         return d
+
+    def test_tcp_fin(self):
+        """Send a list of FIN packets to an address and port pair from inputs."""
+        return self.tcp_flags("F")
+
+    def test_tcp_syn(self):
+        """Send a list of SYN packets to an address and port pair from inputs."""
+        return self.tcp_flags("S")
+
+    def test_tcp_synack(self):
+        """Send a list of SYN/ACK packets to an address and port pair from inputs."""
+        return self.tcp_flags("SA")
+
+    def test_tcp_ack(self):
+        """Send a list of SYN packets to an address and port pair from inputs."""
+        return self.tcp_flags("A")





More information about the tor-commits mailing list