On Wed, Jan 3, 2024 at 11:33 PM <jspewock@iol.unh.edu> wrote:
>
> From: Jeremy Spewock <jspewock@iol.unh.edu>
>
> Added the options to filter out LLDP and ARP packets when
> sniffing for packets with scapy. This was done using BPF filters to
> ensure that the noise these packets provide does not interfere with test
> cases.
>
> Signed-off-by: Jeremy Spewock <jspewock@iol.unh.edu>
> ---
> dts/framework/test_suite.py | 15 +++++++++--
> dts/framework/testbed_model/tg_node.py | 14 ++++++++--
> .../traffic_generator/__init__.py | 7 ++++-
> .../capturing_traffic_generator.py | 22 ++++++++++++++-
> .../testbed_model/traffic_generator/scapy.py | 27 +++++++++++++++++++
> 5 files changed, 79 insertions(+), 6 deletions(-)
>
<snip>
> diff --git a/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py b/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py
> index 0246590333..c1c9facedd 100644
> --- a/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py
> +++ b/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py
<snip>
> @@ -26,6 +27,19 @@ def _get_default_capture_name() -> str:
> return str(uuid.uuid4())
>
>
> +@dataclass(slots=True)
This should also be frozen. If we need a different filter, it's better
to create a new object I think.
> +class PacketFilteringConfig:
> + """The supported filtering options for :class:`CapturingTrafficGenerator`.
> +
> + Attributes:
> + no_lldp: If :data:`True`, LLDP packets will be filtered out when capturing.
> + no_arp: If :data:`True`, ARP packets will be filtered out when capturing.
> + """
> +
> + no_lldp: bool = True
> + no_arp: bool = True
> +
> +
> class CapturingTrafficGenerator(TrafficGenerator):
> """Capture packets after sending traffic.
>
<snip>
> diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
> index 5b60f66237..505de0be94 100644
> --- a/dts/framework/testbed_model/traffic_generator/scapy.py
> +++ b/dts/framework/testbed_model/traffic_generator/scapy.py
<snip>
> @@ -260,11 +263,34 @@ def _send_packets(self, packets: list[Packet], port: Port) -> None:
> packets = [packet.build() for packet in packets]
> self.rpc_server_proxy.scapy_send_packets(packets, port.logical_name)
>
> + def _create_packet_filter(self, filter_config: PacketFilteringConfig) -> str:
> + """Combines filter settings from `filter_config` into a BPF that scapy can use.
> +
> + Scapy allows for the use of Berkeley Packet Filters (BPFs) to filter what packets are
> + collected based on various attributes of the packet.
> +
> + Args:
> + filter_config: Config class that specifies which filters should be applied.
> +
> + Returns:
> + A string representing the combination of BPF filters to be passed to scapy. For
> + example:
> +
> + "ether[12:2] != 0x88cc && ether[12:2] != 0x0806"
> + """
> + bpf_filter: list[str] = []
The type hint here is not needed, so let's make this consistent with
the rest of the code - we don't specify local type hints if they're
not necessary.
> + if filter_config.no_arp:
> + bpf_filter.append("ether[12:2] != 0x0806")
> + if filter_config.no_lldp:
> + bpf_filter.append("ether[12:2] != 0x88cc")
> + return " && ".join(bpf_filter)
> +
> def _send_packets_and_capture(
> self,
> packets: list[Packet],
> send_port: Port,
> receive_port: Port,
> + filter_config: PacketFilteringConfig,
> duration: float,
> capture_name: str = _get_default_capture_name(),
> ) -> list[Packet]:
On Mon, Jan 8, 2024 at 7:01 AM Juraj Linkeš <juraj.linkes@pantheon.tech>
wrote:
> On Wed, Jan 3, 2024 at 11:33 PM <jspewock@iol.unh.edu> wrote:
> >
> > From: Jeremy Spewock <jspewock@iol.unh.edu>
> >
> > Added the options to filter out LLDP and ARP packets when
> > sniffing for packets with scapy. This was done using BPF filters to
> > ensure that the noise these packets provide does not interfere with test
> > cases.
> >
> > Signed-off-by: Jeremy Spewock <jspewock@iol.unh.edu>
> > ---
> > dts/framework/test_suite.py | 15 +++++++++--
> > dts/framework/testbed_model/tg_node.py | 14 ++++++++--
> > .../traffic_generator/__init__.py | 7 ++++-
> > .../capturing_traffic_generator.py | 22 ++++++++++++++-
> > .../testbed_model/traffic_generator/scapy.py | 27 +++++++++++++++++++
> > 5 files changed, 79 insertions(+), 6 deletions(-)
> >
>
> <snip>
>
> > diff --git
> a/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py
> b/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py
> > index 0246590333..c1c9facedd 100644
> > ---
> a/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py
> > +++
> b/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py
> <snip>
> > @@ -26,6 +27,19 @@ def _get_default_capture_name() -> str:
> > return str(uuid.uuid4())
> >
> >
> > +@dataclass(slots=True)
>
> This should also be frozen. If we need a different filter, it's better
> to create a new object I think.
>
> > +class PacketFilteringConfig:
> > + """The supported filtering options for
> :class:`CapturingTrafficGenerator`.
> > +
> > + Attributes:
> > + no_lldp: If :data:`True`, LLDP packets will be filtered out
> when capturing.
> > + no_arp: If :data:`True`, ARP packets will be filtered out when
> capturing.
> > + """
> > +
> > + no_lldp: bool = True
> > + no_arp: bool = True
> > +
> > +
> > class CapturingTrafficGenerator(TrafficGenerator):
> > """Capture packets after sending traffic.
> >
> <snip>
> > diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py
> b/dts/framework/testbed_model/traffic_generator/scapy.py
> > index 5b60f66237..505de0be94 100644
> > --- a/dts/framework/testbed_model/traffic_generator/scapy.py
> > +++ b/dts/framework/testbed_model/traffic_generator/scapy.py
> <snip>
> > @@ -260,11 +263,34 @@ def _send_packets(self, packets: list[Packet],
> port: Port) -> None:
> > packets = [packet.build() for packet in packets]
> > self.rpc_server_proxy.scapy_send_packets(packets,
> port.logical_name)
> >
> > + def _create_packet_filter(self, filter_config:
> PacketFilteringConfig) -> str:
> > + """Combines filter settings from `filter_config` into a BPF
> that scapy can use.
> > +
> > + Scapy allows for the use of Berkeley Packet Filters (BPFs) to
> filter what packets are
> > + collected based on various attributes of the packet.
> > +
> > + Args:
> > + filter_config: Config class that specifies which filters
> should be applied.
> > +
> > + Returns:
> > + A string representing the combination of BPF filters to be
> passed to scapy. For
> > + example:
> > +
> > + "ether[12:2] != 0x88cc && ether[12:2] != 0x0806"
> > + """
> > + bpf_filter: list[str] = []
>
> The type hint here is not needed, so let's make this consistent with
> the rest of the code - we don't specify local type hints if they're
> not necessary.
>
Good catch, this might have been left over from when I was experimenting
with formatting the filters, I'll remove it.
>
> > + if filter_config.no_arp:
> > + bpf_filter.append("ether[12:2] != 0x0806")
> > + if filter_config.no_lldp:
> > + bpf_filter.append("ether[12:2] != 0x88cc")
> > + return " && ".join(bpf_filter)
> > +
> > def _send_packets_and_capture(
> > self,
> > packets: list[Packet],
> > send_port: Port,
> > receive_port: Port,
> > + filter_config: PacketFilteringConfig,
> > duration: float,
> > capture_name: str = _get_default_capture_name(),
> > ) -> list[Packet]:
>
@@ -38,6 +38,7 @@
from .settings import SETTINGS
from .test_result import BuildTargetResult, Result, TestCaseResult, TestSuiteResult
from .testbed_model import Port, PortLink, SutNode, TGNode
+from .testbed_model.traffic_generator import PacketFilteringConfig
from .utils import get_packet_summaries
@@ -208,7 +209,12 @@ def configure_testbed_ipv4(self, restore: bool = False) -> None:
def _configure_ipv4_forwarding(self, enable: bool) -> None:
self.sut_node.configure_ipv4_forwarding(enable)
- def send_packet_and_capture(self, packet: Packet, duration: float = 1) -> list[Packet]:
+ def send_packet_and_capture(
+ self,
+ packet: Packet,
+ filter_config: PacketFilteringConfig = PacketFilteringConfig(),
+ duration: float = 1,
+ ) -> list[Packet]:
"""Send and receive `packet` using the associated TG.
Send `packet` through the appropriate interface and receive on the appropriate interface.
@@ -216,6 +222,7 @@ def send_packet_and_capture(self, packet: Packet, duration: float = 1) -> list[P
Args:
packet: The packet to send.
+ filter_config: The filter to use when capturing packets.
duration: Capture traffic for this amount of time after sending `packet`.
Returns:
@@ -223,7 +230,11 @@ def send_packet_and_capture(self, packet: Packet, duration: float = 1) -> list[P
"""
packet = self._adjust_addresses(packet)
return self.tg_node.send_packet_and_capture(
- packet, self._tg_port_egress, self._tg_port_ingress, duration
+ packet,
+ self._tg_port_egress,
+ self._tg_port_ingress,
+ filter_config,
+ duration,
)
def get_expected_packet(self, packet: Packet) -> Packet:
@@ -15,7 +15,11 @@
from .node import Node
from .port import Port
-from .traffic_generator import CapturingTrafficGenerator, create_traffic_generator
+from .traffic_generator import (
+ CapturingTrafficGenerator,
+ PacketFilteringConfig,
+ create_traffic_generator,
+)
class TGNode(Node):
@@ -53,6 +57,7 @@ def send_packet_and_capture(
packet: Packet,
send_port: Port,
receive_port: Port,
+ filter_config: PacketFilteringConfig = PacketFilteringConfig(),
duration: float = 1,
) -> list[Packet]:
"""Send `packet`, return received traffic.
@@ -65,13 +70,18 @@ def send_packet_and_capture(
packet: The packet to send.
send_port: The egress port on the TG node.
receive_port: The ingress port in the TG node.
+ filter_config: The filter to use when capturing packets.
duration: Capture traffic for this amount of time after sending `packet`.
Returns:
A list of received packets. May be empty if no packets are captured.
"""
return self.traffic_generator.send_packet_and_capture(
- packet, send_port, receive_port, duration
+ packet,
+ send_port,
+ receive_port,
+ filter_config,
+ duration,
)
def close(self) -> None:
@@ -14,11 +14,16 @@
and a capturing traffic generator is required.
"""
+# pylama:ignore=W0611
+
from framework.config import ScapyTrafficGeneratorConfig, TrafficGeneratorType
from framework.exception import ConfigurationError
from framework.testbed_model.node import Node
-from .capturing_traffic_generator import CapturingTrafficGenerator
+from .capturing_traffic_generator import (
+ CapturingTrafficGenerator,
+ PacketFilteringConfig,
+)
from .scapy import ScapyTrafficGenerator
@@ -11,6 +11,7 @@
import uuid
from abc import abstractmethod
+from dataclasses import dataclass
import scapy.utils # type: ignore[import]
from scapy.packet import Packet # type: ignore[import]
@@ -26,6 +27,19 @@ def _get_default_capture_name() -> str:
return str(uuid.uuid4())
+@dataclass(slots=True)
+class PacketFilteringConfig:
+ """The supported filtering options for :class:`CapturingTrafficGenerator`.
+
+ Attributes:
+ no_lldp: If :data:`True`, LLDP packets will be filtered out when capturing.
+ no_arp: If :data:`True`, ARP packets will be filtered out when capturing.
+ """
+
+ no_lldp: bool = True
+ no_arp: bool = True
+
+
class CapturingTrafficGenerator(TrafficGenerator):
"""Capture packets after sending traffic.
@@ -54,6 +68,7 @@ def send_packet_and_capture(
packet: Packet,
send_port: Port,
receive_port: Port,
+ filter_config: PacketFilteringConfig,
duration: float,
capture_name: str = _get_default_capture_name(),
) -> list[Packet]:
@@ -68,6 +83,7 @@ def send_packet_and_capture(
packet: The packet to send.
send_port: The egress port on the TG node.
receive_port: The ingress port in the TG node.
+ filter_config: Filters to apply when capturing packets.
duration: Capture traffic for this amount of time after sending the packet.
capture_name: The name of the .pcap file where to store the capture.
@@ -75,7 +91,7 @@ def send_packet_and_capture(
The received packets. May be empty if no packets are captured.
"""
return self.send_packets_and_capture(
- [packet], send_port, receive_port, duration, capture_name
+ [packet], send_port, receive_port, filter_config, duration, capture_name
)
def send_packets_and_capture(
@@ -83,6 +99,7 @@ def send_packets_and_capture(
packets: list[Packet],
send_port: Port,
receive_port: Port,
+ filter_config: PacketFilteringConfig,
duration: float,
capture_name: str = _get_default_capture_name(),
) -> list[Packet]:
@@ -99,6 +116,7 @@ def send_packets_and_capture(
packets: The packets to send.
send_port: The egress port on the TG node.
receive_port: The ingress port in the TG node.
+ filter_config: Filters to apply when capturing packets.
duration: Capture traffic for this amount of time after sending the packets.
capture_name: The name of the .pcap file where to store the capture.
@@ -113,6 +131,7 @@ def send_packets_and_capture(
packets,
send_port,
receive_port,
+ filter_config,
duration,
)
@@ -126,6 +145,7 @@ def _send_packets_and_capture(
packets: list[Packet],
send_port: Port,
receive_port: Port,
+ filter_config: PacketFilteringConfig,
duration: float,
) -> list[Packet]:
"""The implementation of :method:`send_packets_and_capture`.
@@ -32,6 +32,7 @@
from .capturing_traffic_generator import (
CapturingTrafficGenerator,
+ PacketFilteringConfig,
_get_default_capture_name,
)
@@ -69,6 +70,7 @@ def scapy_send_packets_and_capture(
send_iface: str,
recv_iface: str,
duration: float,
+ sniff_filter: str,
) -> list[bytes]:
"""The RPC function to send and capture packets.
@@ -90,6 +92,7 @@ def scapy_send_packets_and_capture(
iface=recv_iface,
store=True,
started_callback=lambda *args: scapy.all.sendp(scapy_packets, iface=send_iface),
+ filter=sniff_filter,
)
sniffer.start()
time.sleep(duration)
@@ -260,11 +263,34 @@ def _send_packets(self, packets: list[Packet], port: Port) -> None:
packets = [packet.build() for packet in packets]
self.rpc_server_proxy.scapy_send_packets(packets, port.logical_name)
+ def _create_packet_filter(self, filter_config: PacketFilteringConfig) -> str:
+ """Combines filter settings from `filter_config` into a BPF that scapy can use.
+
+ Scapy allows for the use of Berkeley Packet Filters (BPFs) to filter what packets are
+ collected based on various attributes of the packet.
+
+ Args:
+ filter_config: Config class that specifies which filters should be applied.
+
+ Returns:
+ A string representing the combination of BPF filters to be passed to scapy. For
+ example:
+
+ "ether[12:2] != 0x88cc && ether[12:2] != 0x0806"
+ """
+ bpf_filter: list[str] = []
+ if filter_config.no_arp:
+ bpf_filter.append("ether[12:2] != 0x0806")
+ if filter_config.no_lldp:
+ bpf_filter.append("ether[12:2] != 0x88cc")
+ return " && ".join(bpf_filter)
+
def _send_packets_and_capture(
self,
packets: list[Packet],
send_port: Port,
receive_port: Port,
+ filter_config: PacketFilteringConfig,
duration: float,
capture_name: str = _get_default_capture_name(),
) -> list[Packet]:
@@ -277,6 +303,7 @@ def _send_packets_and_capture(
send_port.logical_name,
receive_port.logical_name,
duration,
+ self._create_packet_filter(filter_config),
) # type: ignore[assignment]
scapy_packets = [Ether(packet.data) for packet in xmlrpc_packets]