Skip to content

Commit

Permalink
Merge pull request #1157 from Bastian-Krause/bst/network-test
Browse files Browse the repository at this point in the history
Add RawNetworkInterfaceDriver, labgrid-raw-interface helper and example
  • Loading branch information
jluebbe authored Jan 11, 2024
2 parents 3ab7ab6 + e3ee7f6 commit c301638
Show file tree
Hide file tree
Showing 7 changed files with 396 additions and 0 deletions.
1 change: 1 addition & 0 deletions debian/labgrid.install
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ debian/labgrid-exporter /usr/bin
debian/labgrid-pytest /usr/bin
debian/labgrid-suggest /usr/bin
helpers/labgrid-bound-connect /usr/sbin
helpers/labgrid-raw-interface /usr/sbin
contrib/completion/labgrid-client.bash => /usr/share/bash-completion/completions/labgrid-client
42 changes: 42 additions & 0 deletions doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2960,6 +2960,48 @@ It supports:
- connection sharing (DHCP server with NAT)
- listing DHCP leases (if the client has sufficient permissions)

Binds to:
iface:
- `NetworkInterface`_
- `USBNetworkInterface`_
- `RemoteNetworkInterface`_

Implements:
- None yet

Arguments:
- None

RawNetworkInterfaceDriver
~~~~~~~~~~~~~~~~~~~~~~~~~
This driver allows "raw" control of a network interface (such as Ethernet or
WiFi).

The labgrid-raw-interface helper (``helpers/labgrid-raw-interface``) needs to
be installed in the PATH and usable via sudo without password.
A configuration file ``/etc/labgrid/helpers.yaml`` must be installed on hosts
exporting network interfaces for the RawNetworkInterfaceDriver, e.g.:

.. code-block:: yaml
raw-interface:
denied-interfaces:
- eth1
It supports:
- recording traffic
- replaying traffic
- basic statistic collection

For now, the RawNetworkInterfaceDriver leaves pre-configuration of the exported
network interface to the user, including:
- disabling DHCP
- disabling IPv6 Duplicate Address Detection (DAD) by SLAAC (Stateless
Address Autoconfiguration) and Neighbor Discovery
- disabling Generic Receive Offload (GRO)

This might change in the future.

Binds to:
iface:
- `NetworkInterface`_
Expand Down
14 changes: 14 additions & 0 deletions examples/network-test/env.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
targets:
main:
resources:
NetworkService:
address: 192.168.1.5
username: root
NetworkInterface:
ifname: enp2s0f3
drivers:
SSHDriver: {}
RawNetworkInterfaceDriver: {}
options:
local_iface_to_dut_iface:
enp2s0f3: uplink
55 changes: 55 additions & 0 deletions examples/network-test/pkg-replay-record.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
# Generates an Ethernet frame via scapy using pcap, copies pcap to DUT, replays pcap on interface,
# records frame locally (or on exporter, adjust env.yaml accordingly), and compares both.

import logging
import os
from tempfile import NamedTemporaryFile, TemporaryDirectory

from scapy.all import Ether, Raw, rdpcap, wrpcap, conf

from labgrid import Environment
from labgrid.logging import basicConfig, StepLogger

def generate_frame():
frame = [Ether(dst="11:22:33:44:55:66", src="66:55:44:33:22:11", type=0x9000)]
padding = "\x00" * (conf.min_pkt_size - len(frame))
frame = frame[0] / Raw(load=padding)
return frame


basicConfig(level=logging.INFO)
StepLogger.start()
env = Environment("env.yaml")
target = env.get_target()

netdrv = target.get_driver("RawNetworkInterfaceDriver")
ssh = target.get_driver("SSHDriver")

# get DUT interface
exporter_iface = netdrv.iface.ifname
dut_iface = env.config.get_target_option(target.name, "local_iface_to_dut_iface")[exporter_iface]

# generate test frame
generated_frame = generate_frame()

# write pcap, copy to DUT
remote_pcap = "/tmp/pcap"
with NamedTemporaryFile() as pcap:
wrpcap(pcap.name, generated_frame)
ssh.put(pcap.name, remote_pcap)

# copy recorded pcap from DUT, compare with generated frame
with TemporaryDirectory() as tempdir:
# start record on exporter
tempf = os.path.join(tempdir, "record.pcap")
with netdrv.record(tempf, count=1) as record:
# replay pcap on DUT
ssh.run_check(f"ip link set {dut_iface} up")
ssh.run_check(f"tcpreplay -i {dut_iface} {remote_pcap}")

remote_frame = rdpcap(tempf)
assert remote_frame[0] == generated_frame[0]

print("statistics", netdrv.get_statistics())
print("address", netdrv.get_address())
96 changes: 96 additions & 0 deletions helpers/labgrid-raw-interface
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
#
# Wrapper script to be deployed on machines whose network interfaces should be
# controllable via the RawNetworkInterfaceDriver. A /etc/labgrid/helpers.yaml
# can deny access to network interfaces. See below.
#
# This is intended to be used via sudo. For example, add via visudo:
# %developers ALL = NOPASSWD: /usr/sbin/labgrid-raw-interface

import argparse
import os
import sys

import yaml


def get_denylist():
denylist_file = "/etc/labgrid/helpers.yaml"
try:
with open(denylist_file) as stream:
data = yaml.load(stream, Loader=yaml.SafeLoader)
except (PermissionError, FileNotFoundError, AttributeError) as e:
raise Exception(f"No configuration file ({denylist_file}), inaccessable or invalid yaml") from e

denylist = data.get("raw-interface", {}).get("denied-interfaces", [])

if not isinstance(denylist, list):
raise Exception("No explicit denied-interfaces or not a list, please check your configuration")

denylist.append("lo")

return denylist


def main(program, ifname, count):
if not ifname:
raise ValueError("Empty interface name.")
if any((c == "/" or c.isspace()) for c in ifname):
raise ValueError(f"Interface name '{ifname}' contains invalid characters.")
if len(ifname) > 16:
raise ValueError(f"Interface name '{ifname}' is too long.")

denylist = get_denylist()

if ifname in denylist:
raise ValueError(f"Interface name '{ifname}' is denied in denylist.")

programs = ["tcpreplay", "tcpdump"]
if program not in programs:
raise ValueError(f"Invalid program {program} called with wrapper, valid programs are: {programs}")

args = [
program,
]

if program == "tcpreplay":
args.append(f"--intf1={ifname}")
args.append('-')

if program == "tcpdump":
args.append("-n")
args.append(f"--interface={ifname}")
args.append("-w")
args.append('-')

if count:
args.append("-c")
args.append(str(count))

try:
os.execvp(args[0], args)
except FileNotFoundError as e:
raise RuntimeError(f"Missing {program} binary") from e


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
'-d',
'--debug',
action='store_true',
default=False,
help="enable debug mode"
)
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')
args = parser.parse_args()
try:
main(args.program, args.interface, args.count)
except Exception as e: # pylint: disable=broad-except
if args.debug:
import traceback
traceback.print_exc(file=sys.stderr)
print(f"ERROR: {e}", file=sys.stderr)
exit(1)
1 change: 1 addition & 0 deletions labgrid/driver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from .httpvideodriver import HTTPVideoDriver
from .networkinterfacedriver import NetworkInterfaceDriver
from .provider import HTTPProviderDriver, NFSProviderDriver, TFTPProviderDriver
from .rawnetworkinterfacedriver import RawNetworkInterfaceDriver
from .mqtt import TasmotaPowerDriver
from .manualswitchdriver import ManualSwitchDriver
from .usbtmcdriver import USBTMCDriver
Expand Down
Loading

0 comments on commit c301638

Please sign in to comment.