Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rawnetworkinterface stream #1320

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

JoshuaWatt
Copy link
Contributor

@JoshuaWatt JoshuaWatt commented Jan 19, 2024

Description

Adds support for live streaming packets from a RawNetworkInterfaceDriver. This can be useful for writing tests that parse packets in real time looking for a specific pattern, for example:

import dpkt
    
drv = target.get_driver("RawNetworkInterfaceDriver")
    
with drv.record("-", timeout=60) as p:
    pcap = dpkt.pcap.Reader(p.stdout)
    for timestamp, buf in pcap:
        eth = dpkt.ethernet.Ethernet(buf)

        if _some_criteria_:
            break
    else:
        assert False, "Packet not found!"

Checklist

  • Documentation for the feature
  • Tests for the feature
  • The arguments and description in doc/configuration.rst have been updated
  • Add a section on how to use the feature to doc/usage.rst
  • Add a section on how to use the feature to doc/development.rst
  • PR has been tested
  • Man pages have been regenerated

@JoshuaWatt
Copy link
Contributor Author

Of note, we've had a local driver that was very similar to RawNetworkInterfaceDriver for some time that was used to stream packets; the intention of this patch is to bring it in line with the functionality in that driver so we can eliminate it. For posterity, here is our driver:

import attr
import subprocess
import dpkt
from contextlib import contextmanager

from labgrid import target_factory
from labgrid.driver import Driver


@target_factory.reg_driver
@attr.s(eq=False)
class InterfaceCaptureDriver(Driver):
    """Driver to obtain tcpdump output from device's network"""

    bindings = {
        "iface": {"RemoteNetworkInterface", "USBNetworkInterface", "NetworkInterface"},
    }

    @contextmanager
    def live_capture(self, timeout=None):
        """
        Perform live capture of traffic from device's network.

        :param int timeout: When specified, performs capture for ``timeout`` seconds. \
            Defaults to `None`.

        :yields: A `dpkt.pcap.Reader` object of the captured network traffic
        """
        extra = getattr(self.iface, "extra", {})
        command = ["sudo", "tcpdump", "-i", self.iface.ifname, "-U", "-s0", "-w", "-"]

        if timeout is not None:
            command.extend(["-G", str(timeout), "-W", "1"])

        if "proxy_required" in extra:
            command = ["ssh", extra["proxy"], "--"] + command

        with subprocess.Popen(command, stdout=subprocess.PIPE) as p:
            yield dpkt.pcap.Reader(p.stdout)

Copy link

codecov bot commented Jan 19, 2024

Codecov Report

Attention: Patch coverage is 14.28571% with 12 lines in your changes are missing coverage. Please review.

Project coverage is 62.7%. Comparing base (722838b) to head (d68686a).
Report is 9 commits behind head on master.

❗ Current head d68686a differs from pull request most recent head fdeb30b. Consider uploading reports for the commit fdeb30b to get more accurate results

Files Patch % Lines
labgrid/driver/rawnetworkinterfacedriver.py 14.2% 12 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##           master   #1320     +/-   ##
========================================
+ Coverage    62.2%   62.7%   +0.5%     
========================================
  Files         164     163      -1     
  Lines       12191   12008    -183     
========================================
- Hits         7584    7534     -50     
+ Misses       4607    4474    -133     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Member

@Bastian-Krause Bastian-Krause left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Looks good overall.

In the commit message of d68686a ("rawnetworkinterfacedriver: Implement live streaming"):

-with drv.record("-", timeout=60) as p
+with drv.record("-", timeout=60) as p:

with open(filename, "wb") as outdata:
self._record_handle = subprocess.Popen(cmd, stdout=outdata, stderr=subprocess.PIPE)
if filename == "-":
self._record_handle = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
Copy link
Member

@Bastian-Krause Bastian-Krause Mar 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be good to also redirect stderr to a pipe here, so a test can also make use of that? But don't make it optional.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not a good idea (IMHO). If you make stderr=subprocess.PIPE, then every consumer of the subprocess must periodically read from the stderr pipe, otherwise if it fills up it will deadlock the process. Making it mandatory would make the code consuming this data have to do that, which is annoying. Better would be to make it optional, although this still has problems because if you do stdout=subprocess.PIPE, stderr=subprocess.PIPE, a consumer would need to poll() on both stderr and stdout to prevent deadlock. Anyway, given how tricky this is to get correct, I think leaving it is the correct answer for now. If an error occurs, the tcpdump error output will be visible in the logs/stderr/etc. which is sufficient to debug the problem without a huge burden on the consuming code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My expectation was that tcpdump doesn't produce too much output on stderr, but maybe that's too naive. I'm okay with keeping it as is.

helpers/labgrid-raw-interface Outdated Show resolved Hide resolved
helpers/labgrid-raw-interface Outdated Show resolved Hide resolved
Comment on lines 118 to 120
# A timeout is expected if the context is exited early while
# piping to stdout
pass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing here is only correct for filename == "-", right?

helpers/labgrid-raw-interface Outdated Show resolved Hide resolved
helpers/labgrid-raw-interface Outdated Show resolved Hide resolved
@@ -85,9 +96,10 @@ if __name__ == "__main__":
parser.add_argument('program', type=str, help='program to run, either tcpreplay or tcpdump')
parser.add_argument('interface', type=str, help='interface name')
parser.add_argument('count', nargs="?", type=int, default=None, help='amount of frames to capture while recording')
parser.add_argument('--duration', type=int, default=None, help="Amount of time to capture while recording")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for calling this "duration" instead of "timeout"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duration was chosen because it's distinct from the timeout argument used in rawnetworkinterface.py. It can be confusing have the same name meaning different things in that file, so I thought "duration" was a more descriptive name

labgrid/driver/rawnetworkinterfacedriver.py Outdated Show resolved Hide resolved
Comment on lines 71 to 81
# The duration is implemented by specifying the number of seconds
# before rotating the dump file, but limiting the number of files
# to 1
args.append("-G")
args.append(str(duration))
args.append("-W")
args.append("1")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect this might fail in the future when used in combination with writing to stdout. Handling that in the helper would mean using a subprocess instead os.execvp, so that can wait util that actually is a problem.

labgrid/driver/rawnetworkinterfacedriver.py Outdated Show resolved Hide resolved
Comment on lines -108 to +118
yield self.start_record(filename, count=count)
yield self.start_record(filename, count=count, timeout=timeout)
finally:
self.stop_record(timeout=timeout)
# If live streaming packets, there is no reason to wait for tcpdump
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes the behavior to starting the timeout when beginning the capture instead of at the end of the context. I'm not sure if anyone cares, though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that change in behavior is okay.

labgrid/driver/rawnetworkinterfacedriver.py Outdated Show resolved Hide resolved
@Bastian-Krause
Copy link
Member

@JoshuaWatt I'm interested in using the changes in this PR in our network tests. Could you have a look at the review comments? If you don't have the time to integrate the changes, I'd be willing to take this over.

@JoshuaWatt
Copy link
Contributor Author

@JoshuaWatt I'm interested in using the changes in this PR in our network tests. Could you have a look at the review comments? If you don't have the time to integrate the changes, I'd be willing to take this over.

I'll take a look this week. I was AFK for EOSS

Reworks the way that timeouts are handled so that instead of terminating
the process in stop_record, tcpdump will exit after a set time. This is
will allow for live streaming of packets in a future patch

Signed-off-by: Joshua Watt <Joshua.Watt@garmin.com>
Adds support for live streaming of captured packets such that they can
be processed in real time by tests instead of needing to capture for a
set period of time time and do post-processing.

As an example:

```python
import dpkt

drv = target.get_driver("RawNetworkInterfaceDriver")

with drv.record(None, timeout=60) as p:
    pcap = dpkt.pcap.Reader(p.stdout)
    for timestamp, buf in pcap:
        eth = dpkt.ethernet.Ethernet(buf)
        ....
```

Signed-off-by: Joshua Watt <Joshua.Watt@garmin.com>
Copy link
Member

@Bastian-Krause Bastian-Krause left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the feedback. The changes look good to me. I'll try to test this within the next weeks and add my approval then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants