Skip to content

Commit adc614a

Browse files
authored
Merge pull request #1320 from JoshuaWatt/rawnetworkinterface-stream
Rawnetworkinterface stream
2 parents ec55819 + dcfa087 commit adc614a

File tree

2 files changed

+66
-40
lines changed

2 files changed

+66
-40
lines changed

helpers/labgrid-raw-interface

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,18 @@ def get_denylist():
3232
return denylist
3333

3434

35-
def main(program, ifname, count):
36-
if not ifname:
35+
def main(program, options):
36+
if not options.ifname:
3737
raise ValueError("Empty interface name.")
38-
if any((c == "/" or c.isspace()) for c in ifname):
39-
raise ValueError(f"Interface name '{ifname}' contains invalid characters.")
40-
if len(ifname) > 16:
41-
raise ValueError(f"Interface name '{ifname}' is too long.")
38+
if any((c == "/" or c.isspace()) for c in options.ifname):
39+
raise ValueError(f"Interface name '{options.ifname}' contains invalid characters.")
40+
if len(options.ifname) > 16:
41+
raise ValueError(f"Interface name '{options.ifname}' is too long.")
4242

4343
denylist = get_denylist()
4444

45-
if ifname in denylist:
46-
raise ValueError(f"Interface name '{ifname}' is denied in denylist.")
45+
if options.ifname in denylist:
46+
raise ValueError(f"Interface name '{options.ifname}' is denied in denylist.")
4747

4848
programs = ["tcpreplay", "tcpdump"]
4949
if program not in programs:
@@ -54,18 +54,30 @@ def main(program, ifname, count):
5454
]
5555

5656
if program == "tcpreplay":
57-
args.append(f"--intf1={ifname}")
58-
args.append('-')
57+
args.append(f"--intf1={options.ifname}")
58+
args.append("-")
5959

6060
if program == "tcpdump":
6161
args.append("-n")
62-
args.append(f"--interface={ifname}")
62+
args.append(f"--interface={options.ifname}")
63+
# Write out each packet as it is received
64+
args.append("--packet-buffered")
65+
# Capture complete packets (for compatibility with older tcpdump versions)
66+
args.append("--snapshot-length=0")
6367
args.append("-w")
64-
args.append('-')
68+
args.append("-")
6569

66-
if count:
70+
if options.count:
6771
args.append("-c")
68-
args.append(str(count))
72+
args.append(str(options.count))
73+
74+
if options.timeout:
75+
# The timeout is implemented by specifying the number of seconds before rotating the
76+
# dump file, but limiting the number of files to 1
77+
args.append("-G")
78+
args.append(str(options.timeout))
79+
args.append("-W")
80+
args.append("1")
6981

7082
try:
7183
os.execvp(args[0], args)
@@ -75,22 +87,28 @@ def main(program, ifname, count):
7587

7688
if __name__ == "__main__":
7789
parser = argparse.ArgumentParser()
78-
parser.add_argument(
79-
'-d',
80-
'--debug',
81-
action='store_true',
82-
default=False,
83-
help="enable debug mode"
90+
parser.add_argument("-d", "--debug", action="store_true", default=False, help="enable debug mode")
91+
subparsers = parser.add_subparsers(dest="program", help="program to run")
92+
93+
# tcpdump
94+
tcpdump_parser = subparsers.add_parser("tcpdump")
95+
tcpdump_parser.add_argument("ifname", type=str, help="interface name")
96+
tcpdump_parser.add_argument("count", type=int, default=None, help="amount of frames to capture while recording")
97+
tcpdump_parser.add_argument(
98+
"--timeout", type=int, default=None, help="Amount of time to capture while recording. 0 means capture forever"
8499
)
85-
parser.add_argument('program', type=str, help='program to run, either tcpreplay or tcpdump')
86-
parser.add_argument('interface', type=str, help='interface name')
87-
parser.add_argument('count', nargs="?", type=int, default=None, help='amount of frames to capture while recording')
100+
101+
# tcpreplay
102+
tcpreplay_parser = subparsers.add_parser("tcpreplay")
103+
tcpreplay_parser.add_argument("ifname", type=str, help="interface name")
104+
88105
args = parser.parse_args()
89106
try:
90-
main(args.program, args.interface, args.count)
107+
main(args.program, args)
91108
except Exception as e: # pylint: disable=broad-except
92109
if args.debug:
93110
import traceback
111+
94112
traceback.print_exc(file=sys.stderr)
95113
print(f"ERROR: {e}", file=sys.stderr)
96114
exit(1)

labgrid/driver/rawnetworkinterfacedriver.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,15 @@ def _stop(self, proc, *, timeout=None):
5353
)
5454

5555
@Driver.check_active
56-
@step(args=["filename", "count"])
57-
def start_record(self, filename, *, count=None):
56+
@step(args=["filename", "count", "timeout"])
57+
def start_record(self, filename, *, count=None, timeout=None):
5858
"""
5959
Starts tcpdump on bound network interface resource.
6060
6161
Args:
62-
filename (str): name of a file to record to
62+
filename (str): name of a file to record to, or None to record to stdout
6363
count (int): optional, exit after receiving this many number of packets
64+
timeout (int): optional, number of seconds to capture packets before tcpdump exits
6465
Returns:
6566
Popen object of tcpdump process
6667
"""
@@ -69,9 +70,15 @@ def start_record(self, filename, *, count=None):
6970
cmd = ["tcpdump", self.iface.ifname]
7071
if count is not None:
7172
cmd.append(str(count))
73+
if timeout is not None:
74+
cmd.append("--timeout")
75+
cmd.append(str(timeout))
7276
cmd = self._wrap_command(cmd)
73-
with open(filename, "wb") as outdata:
74-
self._record_handle = subprocess.Popen(cmd, stdout=outdata, stderr=subprocess.PIPE)
77+
if filename is None:
78+
self._record_handle = subprocess.Popen(cmd, stdout=subprocess.PIPE)
79+
else:
80+
with open(filename, "wb") as outdata:
81+
self._record_handle = subprocess.Popen(cmd, stdout=outdata, stderr=subprocess.PIPE)
7582
return self._record_handle
7683

7784
@Driver.check_active
@@ -86,6 +93,11 @@ def stop_record(self, *, timeout=None):
8693
"""
8794
try:
8895
self._stop(self._record_handle, timeout=timeout)
96+
except subprocess.TimeoutExpired:
97+
# If live streaming packets, there is no reason to wait for tcpdump
98+
# to finish, so expect a timeout if piping to stdout
99+
if self._record_handle.stdout is None:
100+
raise
89101
finally:
90102
self._record_handle = None
91103

@@ -97,17 +109,18 @@ def record(self, filename, *, count=None, timeout=None):
97109
Either count or timeout must be specified.
98110
99111
Args:
100-
filename (str): name of a file to record to
112+
filename (str): name of a file to record to, or None to live stream packets
101113
count (int): optional, exit after receiving this many number of packets
102-
timeout (int): optional, maximum number of seconds to wait for the tcpdump process to
103-
terminate
114+
timeout (int): optional, number of seconds to capture packets before tcpdump exits
115+
Returns:
116+
Popen object of tcpdump process. If filename is None, packets can be read from stdout
104117
"""
105118
assert count or timeout
106119

107120
try:
108-
yield self.start_record(filename, count=count)
121+
yield self.start_record(filename, count=count, timeout=timeout)
109122
finally:
110-
self.stop_record(timeout=timeout)
123+
self.stop_record(timeout=0 if filename is None else None)
111124

112125
@Driver.check_active
113126
@step(args=["filename"])
@@ -170,12 +183,7 @@ def get_statistics(self):
170183
"""
171184
Returns basic interface statistics of bound network interface resource.
172185
"""
173-
cmd = self.iface.command_prefix + [
174-
"ip",
175-
"--json",
176-
"-stats", "-stats",
177-
"link", "show",
178-
self.iface.ifname]
186+
cmd = self.iface.command_prefix + ["ip", "--json", "-stats", "-stats", "link", "show", self.iface.ifname]
179187
output = processwrapper.check_output(cmd)
180188
return json.loads(output)[0]
181189

0 commit comments

Comments
 (0)