diff --git a/extra-requirements.txt b/extra-requirements.txt
index 80bfa1260..8f409b2b4 100644
--- a/extra-requirements.txt
+++ b/extra-requirements.txt
@@ -1,6 +1,7 @@
 # Additional requirements not in openstack/requirements project
 
 ansi2html                # LGPLv3+
+dpkt                     # BSD
 pandas                   # BSD
 podman-py                # Apache-2.0
 pytest-cov               # MIT
diff --git a/lower-constraints.txt b/lower-constraints.txt
index df1b0ec47..2d3c01662 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -2,6 +2,7 @@
 
 decorator===4.4.2
 docker==4.4.1
+dpkt >= 1.8.8
 fixtures==3.0.0
 Jinja2==2.11.2
 keystoneauth1==4.3.0
diff --git a/tobiko/shell/ip.py b/tobiko/shell/ip.py
index ba013296d..abf926e68 100644
--- a/tobiko/shell/ip.py
+++ b/tobiko/shell/ip.py
@@ -102,6 +102,15 @@ def list_network_interfaces(**execute_params):
     return interfaces
 
 
+def get_network_main_route_device(dest_ip, **execute_params):
+    output = execute_ip(['route', 'get', dest_ip], **execute_params)
+    if output:
+        for line in output.splitlines():
+            fields = line.strip().split()
+            device_index = fields.index('dev') + 1
+            return fields[device_index]
+
+
 IP_COMMAND = sh.shell_command(['/sbin/ip'])
 
 
diff --git a/tobiko/shell/sh/_execute.py b/tobiko/shell/sh/_execute.py
index 83e837989..3d8b8f71d 100644
--- a/tobiko/shell/sh/_execute.py
+++ b/tobiko/shell/sh/_execute.py
@@ -114,6 +114,7 @@ class ShellExecuteResult(collections.namedtuple(
 
 
 def _indent(text, space='    ', newline='\n'):
+    text = str(text)
     return space + (newline + space).join(text.split(newline))
 
 
diff --git a/tobiko/shell/sh/_process.py b/tobiko/shell/sh/_process.py
index d98d85986..3af7cfa0e 100644
--- a/tobiko/shell/sh/_process.py
+++ b/tobiko/shell/sh/_process.py
@@ -425,7 +425,12 @@ def merge_dictionaries(*dictionaries):
 
 def str_from_stream(stream):
     if stream is not None:
-        return str(stream)
+        try:
+            return str(stream)
+        except UnicodeDecodeError:
+            LOG.exception('Unable to decode as a string - '
+                          'Returning the raw data')
+            return stream.data
     else:
         return None
 
diff --git a/tobiko/shell/tcpdump/__init__.py b/tobiko/shell/tcpdump/__init__.py
new file mode 100644
index 000000000..31a6c1237
--- /dev/null
+++ b/tobiko/shell/tcpdump/__init__.py
@@ -0,0 +1,26 @@
+# Copyright (c) 2021 Red Hat, Inc.
+#
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+from __future__ import absolute_import
+
+from tobiko.shell.tcpdump import _assert
+from tobiko.shell.tcpdump import _execute
+
+
+assert_pcap_is_empty = _assert.assert_pcap_is_empty
+assert_pcap_is_not_empty = _assert.assert_pcap_is_not_empty
+
+start_capture = _execute.start_capture
+get_pcap = _execute.get_pcap
diff --git a/tobiko/shell/tcpdump/_assert.py b/tobiko/shell/tcpdump/_assert.py
new file mode 100644
index 000000000..c1c1f3ac5
--- /dev/null
+++ b/tobiko/shell/tcpdump/_assert.py
@@ -0,0 +1,45 @@
+# Copyright (c) 2021 Red Hat, Inc.
+#
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+from __future__ import absolute_import
+from __future__ import division
+
+import dpkt
+from oslo_log import log
+
+import tobiko
+
+
+LOG = log.getLogger(__name__)
+
+
+def assert_pcap_content(pcap: dpkt.pcap.Reader, expect_empty: bool):
+    actual_empty = True
+    for _ in pcap:
+        actual_empty = False
+        break
+    testcase = tobiko.get_test_case()
+    LOG.debug(f'Is the obtained pcap file empty? {actual_empty}')
+    testcase.assertEqual(expect_empty, actual_empty)
+
+
+def assert_pcap_is_empty(pcap: dpkt.pcap.Reader):
+    LOG.debug('This test expects an empty pcap capture')
+    assert_pcap_content(pcap, True)
+
+
+def assert_pcap_is_not_empty(pcap: dpkt.pcap.Reader):
+    LOG.debug('This test expects a non-empty pcap capture')
+    assert_pcap_content(pcap, False)
diff --git a/tobiko/shell/tcpdump/_execute.py b/tobiko/shell/tcpdump/_execute.py
new file mode 100644
index 000000000..6090de391
--- /dev/null
+++ b/tobiko/shell/tcpdump/_execute.py
@@ -0,0 +1,71 @@
+# Copyright (c) 2021 Red Hat, Inc.
+#
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+from __future__ import absolute_import
+from __future__ import division
+
+import io
+
+import dpkt
+from oslo_log import log
+
+from tobiko.shell.tcpdump import _interface
+from tobiko.shell.tcpdump import _parameters
+from tobiko.shell import sh
+from tobiko.shell import ssh
+
+
+LOG = log.getLogger(__name__)
+
+
+def start_capture(capture_file: str,
+                  interface: str = None,
+                  capture_filter: str = None,
+                  capture_timeout: int = None,
+                  ssh_client: ssh.SSHClientType = None) \
+        -> sh.ShellProcessFixture:
+
+    parameters = _parameters.tcpdump_parameters(
+        capture_file=capture_file,
+        interface=interface,
+        capture_filter=capture_filter,
+        capture_timeout=capture_timeout)
+
+    command = _interface.get_tcpdump_command(parameters)
+
+    # when ssh_client is None, an ssh session is created on localhost
+
+    # using a process we run a fire and forget tcpdump command
+    process = sh.process(command=command,
+                         ssh_client=ssh_client,
+                         sudo=True)
+    process.execute()
+    return process
+
+
+def stop_capture(process):
+    process.kill()
+    process.close()
+
+
+def get_pcap(process,
+             capture_file: str,
+             ssh_client: ssh.SSHClientType = None) -> dpkt.pcap.Reader:
+    stop_capture(process)
+
+    stdout = sh.execute(
+        f'cat {capture_file}', ssh_client=ssh_client, sudo=True).stdout
+    pcap = dpkt.pcap.Reader(io.BytesIO(stdout))
+    return pcap
diff --git a/tobiko/shell/tcpdump/_interface.py b/tobiko/shell/tcpdump/_interface.py
new file mode 100644
index 000000000..cedfd20a3
--- /dev/null
+++ b/tobiko/shell/tcpdump/_interface.py
@@ -0,0 +1,46 @@
+# Copyright (c) 2021 Red Hat, Inc.
+#
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+from __future__ import absolute_import
+
+from tobiko.shell.tcpdump import _parameters
+
+
+def get_tcpdump_command(parameters: _parameters.TcpdumpParameters):
+    interface = TcpdumpInterface()
+    return interface.get_tcpdump_command(parameters)
+
+
+class TcpdumpInterface:
+
+    def get_tcpdump_command(
+            self, parameters: _parameters.TcpdumpParameters) -> str:
+        command = 'tcpdump -s0 -Un'
+        if parameters.capture_timeout is not None:
+            command = f'timeout {parameters.capture_timeout} ' + command
+        options = self.get_tcpdump_options(parameters=parameters)
+        return command + ' ' + options
+
+    def get_tcpdump_options(
+            self,
+            parameters: _parameters.TcpdumpParameters) -> str:
+        options = f'-w {parameters.capture_file}'
+        if parameters.interface is not None:
+            options += f' -i {parameters.interface}'
+        else:
+            options += ' -i any'
+        if parameters.capture_filter is not None:
+            options += f' {parameters.capture_filter}'
+        return options
diff --git a/tobiko/shell/tcpdump/_parameters.py b/tobiko/shell/tcpdump/_parameters.py
new file mode 100644
index 000000000..d9e1ec576
--- /dev/null
+++ b/tobiko/shell/tcpdump/_parameters.py
@@ -0,0 +1,38 @@
+# Copyright (c) 2021 Red Hat, Inc.
+#
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+from __future__ import absolute_import
+
+import typing
+
+
+class TcpdumpParameters(typing.NamedTuple):
+    capture_file: str
+    interface: typing.Optional[str] = None
+    capture_filter: typing.Optional[str] = None
+    capture_timeout: typing.Optional[int] = None
+
+
+def tcpdump_parameters(
+        capture_file: str,
+        interface: str = None,
+        capture_filter: str = None,
+        capture_timeout: int = None):
+    """Get tcpdump parameters
+    """
+    return TcpdumpParameters(capture_file=capture_file,
+                             interface=interface,
+                             capture_filter=capture_filter,
+                             capture_timeout=capture_timeout)
diff --git a/tobiko/tests/scenario/neutron/test_qos.py b/tobiko/tests/scenario/neutron/test_qos.py
index 580ab0191..057fe670e 100644
--- a/tobiko/tests/scenario/neutron/test_qos.py
+++ b/tobiko/tests/scenario/neutron/test_qos.py
@@ -14,18 +14,24 @@
 #    under the License.
 from __future__ import absolute_import
 
+import time
+
 from oslo_log import log
 import testtools
 
 import tobiko
+from tobiko import config
 from tobiko.openstack import keystone
-from tobiko.openstack import stacks
 from tobiko.openstack import neutron
+from tobiko.openstack import stacks
+from tobiko.shell import ip
 from tobiko.shell import iperf3
 from tobiko.shell import ping
 from tobiko.shell import sh
+from tobiko.shell import tcpdump
 
 
+CONF = config.CONF
 LOG = log.getLogger(__name__)
 
 
@@ -39,8 +45,34 @@ class QoSNetworkTest(testtools.TestCase):
     policy = tobiko.required_setup_fixture(stacks.QosPolicyStackFixture)
     server = tobiko.required_setup_fixture(stacks.QosServerStackFixture)
 
-    def test_ping(self):
+    def test_ping_dscp(self):
+        capture_file = sh.execute('mktemp', sudo=True).stdout.strip()
+        interface = ip.get_network_main_route_device(
+            self.server.floating_ip_address)
+
+        # IPv4 tcpdump DSCP filters explanation:
+        # ip[1] refers to the byte 1 (the TOS byte) of the IP header
+        # 0xfc = 11111100 is the mask to get only DSCP value from the ToS
+        # As DSCP mark is most significant 6 bits we do right shift (>>)
+        # twice in order to divide by 4 and compare with the decimal value
+        # See details at http://darenmatthews.com/blog/?p=1199
+        dscp_mark = CONF.tobiko.neutron.dscp_mark
+        capture_filter = (f"'(ip src {self.server.floating_ip_address} and "
+                          f"(ip[1] & 0xfc) >> 2 == {dscp_mark})'")
+
+        # start a capture
+        process = tcpdump.start_capture(
+            capture_file=capture_file,
+            interface=interface,
+            capture_filter=capture_filter,
+            capture_timeout=60)
+        time.sleep(1)
+        # send a ping to the server
         ping.assert_reachable_hosts([self.server.floating_ip_address],)
+        # stop tcpdump and get the pcap capture
+        pcap = tcpdump.get_pcap(process, capture_file=capture_file)
+        # check the capture is not empty
+        tcpdump.assert_pcap_is_not_empty(pcap=pcap)
 
     def test_network_qos_policy_id(self):
         """Verify network policy ID"""