From 461be07d8c2f3a1d5d7817bf424c6e78141aa37f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20De=20Keersmaeker?=
 <francois.dekeersmaeker@uclouvain.be>
Date: Wed, 31 May 2023 10:40:14 +0200
Subject: [PATCH] If packet not tweaked, loop on lower layers until edit or not
 supported

---
 src/packet/Packet.py    |  32 +++++++++++++++++++++++---------
 src/packet/Transport.py |   5 +++--
 src/pcap_tweaker.py     |  35 ++++++++++++++++++++---------------
 traces/udp-stream.pcap  | Bin 0 -> 6636 bytes
 4 files changed, 46 insertions(+), 26 deletions(-)
 create mode 100644 traces/udp-stream.pcap

diff --git a/src/packet/Packet.py b/src/packet/Packet.py
index 9808fe0..85d3d20 100644
--- a/src/packet/Packet.py
+++ b/src/packet/Packet.py
@@ -97,17 +97,20 @@ class Packet:
 
 
     @classmethod
-    def init_packet(c, packet: scapy.Packet, id: int = 0) -> Packet:
+    def init_packet(c, packet: scapy.Packet, id: int = 0, last_layer_index: int = -1) -> Packet:
         """
         Factory method to create a packet of a given protocol.
 
         :param packet: Scapy Packet to be edited.
-        :param id: Packet integer identifier.
+        :param id: [Optional] Packet integer identifier. Default is 0.
+        :param last_layer_index: [Optional] Index of the last layer of the packet.
+                                 If not specified, it will be calculated.
         :return: Packet of given protocol,
                  or generic Packet if protocol is not supported.
         """
         # Try creating specific packet if possible
-        last_layer_index = Packet.get_last_layer_index(packet)
+        if last_layer_index == -1:
+            last_layer_index = Packet.get_last_layer_index(packet)
         for i in range(last_layer_index, -1, -1):
             layer = packet.getlayer(i)
             try:
@@ -120,7 +123,7 @@ class Packet:
                     protocol = Packet.protocols.get(protocol, protocol)
                 module = importlib.import_module(f"packet.{protocol}")
                 cls = getattr(module, protocol)
-                return cls(packet, id)
+                return cls(packet, id, i)
             except ModuleNotFoundError:
                 # Layer protocol not supported
                 continue
@@ -128,19 +131,21 @@ class Packet:
         raise ValueError(f"No supported protocol found for packet: {packet.summary()}")
 
 
-    def __init__(self, packet: scapy.Packet, id: int = 0) -> None:
+    def __init__(self, packet: scapy.Packet, id: int = 0, last_layer_index: int = -1) -> None:
         """
         Generic packet constructor.
 
         :param packet: Scapy Packet to be edited.
         :param id: Packet integer identifier.
+        :param last_layer_index: [Optional] Index of the last layer of the packet.
+                                 If not specified, it will be calculated.
         """
         self.id = id
         self.packet = packet
-        try:
-            self.layer = packet.getlayer(self.name)
-        except AttributeError:
-            self.layer = packet.lastlayer()
+        self.layer_index = last_layer_index if last_layer_index != -1 else Packet.get_last_layer_index(packet)
+        self.layer = packet.getlayer(self.name)
+        if self.layer is None:
+            self.layer = packet.getlayer(self.layer_index)
 
     
     def get_packet(self) -> scapy.Packet:
@@ -171,6 +176,15 @@ class Packet:
         return len(self.packet.getlayer(layer))
     
 
+    def get_layer_index(self) -> int:
+        """
+        Get packet layer index.
+
+        :return: Packet layer index.
+        """
+        return self.layer_index
+    
+
     def rebuild(self) -> None:
         """
         Rebuild packet.
diff --git a/src/packet/Transport.py b/src/packet/Transport.py
index 444149d..9109bac 100644
--- a/src/packet/Transport.py
+++ b/src/packet/Transport.py
@@ -20,8 +20,9 @@ class Transport(Packet):
 
     def tweak(self) -> dict:
         """
-        Randomly edit destination or source port,
-        in this order of priority.
+        If one of the ports is a well-known port,
+        randomly edit destination or source port,
+        in this respective order of priority.
 
         :return: Dictionary containing tweak information,
                  or None if no tweak was performed.
diff --git a/src/pcap_tweaker.py b/src/pcap_tweaker.py
index 47a7df5..16dbd65 100644
--- a/src/pcap_tweaker.py
+++ b/src/pcap_tweaker.py
@@ -96,25 +96,30 @@ def tweak_pcaps(pcaps: list, output: str, random_range: int = 1, packet_numbers:
 
                 if must_edit_packet(i, packet_numbers, random_range):
                     # Edit packet, if possible
-                    try:
-                        my_packet = Packet.init_packet(packet, i)
-                    except ValueError:
-                        # No supported protocol found in packet, skip it
-                        new_packets.append(rebuild_packet(packet))
-                        pass
-                    else:
-                        d = my_packet.tweak()
-                        new_packets.append(my_packet.get_packet())
-                        if d is not None:
-                            writer.writerow(d)
-                    finally:
-                        i += 1
+                    last_layer_index = Packet.get_last_layer_index(packet)
+                    while True:
+                        try:
+                            my_packet = Packet.init_packet(packet, i, last_layer_index)
+                        except ValueError:
+                            # No supported protocol found in packet, skip it
+                            new_packets.append(rebuild_packet(packet))
+                            break
+                        else:
+                            d = my_packet.tweak()
+                            if d is None:
+                                # Packet was not edited, try editing one layer lower
+                                last_layer_index = my_packet.get_layer_index() - 1
+                            else:
+                                # Packet was edited
+                                new_packets.append(my_packet.get_packet())
+                                writer.writerow(d)
+                                break
                 else:
                     # Packet won't be edited
-                    # Go to next packet
-                    i += 1
                     new_packets.append(rebuild_packet(packet))
 
+                i += 1
+
         # Write output PCAP file
         output_pcap = ""
         if output is not None and len(pcaps) == 1:
diff --git a/traces/udp-stream.pcap b/traces/udp-stream.pcap
new file mode 100644
index 0000000000000000000000000000000000000000..c9a4ab0e54dce3380a0474f3a3db09733a2174a9
GIT binary patch
literal 6636
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

literal 0
HcmV?d00001

-- 
GitLab