How to measure the packet rate (frequency) using tcpdump
and nano-second-resolution timestamps
Quick Summary:
For both options shown below, update the num_packets
values and the tcpdump
commands is all, in these "one-liner" command blobs:
Option 1:
This always works and is pretty good:
num_packets="25"; \
time_start_sec="$(date +"%s.%N")"; \
time sudo tcpdump -i any -c "$num_packets" \
"(src 127.0.0.1 and port 40000) and (dst 127.0.0.1 and port 40001)"; \
time_end_sec="$(date +"%s.%N")"; \
dt_sec="$(bc <<< "scale=20; $time_end_sec - $time_start_sec")"; \
packet_rate_hz="$(bc <<< "scale=20; $num_packets / $dt_sec")"; \
printf "\n%s%.3f\n\n" "packet_rate_hz = " "$packet_rate_hz"
Option 2 [best]:
This works with most, if not all, tcpdump
commands which do not include the -t
flag, and is much better!:
num_packets="10"; \
time sudo tcpdump -l -i any -c "$num_packets" \
"(src 127.0.0.1 and port 40000) and (dst 127.0.0.1 and port 40001)" \
| tee tcpdump.txt; \
time_start_sec="$(grep -o \
'[0-9][0-9]:[0-9][0-9]:[0-9][0-9]\.[0-9]*' \
"tcpdump.txt" \
| head -n 1 | grep -o '[0-9][0-9]\.[0-9]*')"; \
time_end_sec="$(grep -o \
'[0-9][0-9]:[0-9][0-9]:[0-9][0-9]\.[0-9]*' \
"tcpdump.txt" \
| tail -n 1 | grep -o '[0-9][0-9]\.[0-9]*')"; \
dt_sec="$(bc <<< "scale=20; $time_end_sec - $time_start_sec")"; \
packet_rate_hz="$(bc <<< "scale=20; ($num_packets-1) / $dt_sec")"; \
printf "\n%s%.4f\n\n" "packet_rate_hz = " "$packet_rate_hz"
Option 1: manually time the whole process using nanosecond-resolution timestamps from date
This works ok. I simply tell tcpdump
to grab a certain number of packets, time the whole thing, and calculate the packet rate:
num_packets="25"; \
time_start_sec="$(date +"%s.%N")"; \
time sudo tcpdump -i any -c "$num_packets" \
"(src 127.0.0.1 and port 40000) and (dst 127.0.0.1 and port 40001)"; \
time_end_sec="$(date +"%s.%N")"; \
dt_sec="$(bc <<< "scale=20; $time_end_sec - $time_start_sec")"; \
packet_rate_hz="$(bc <<< "scale=20; $num_packets / $dt_sec")"; \
printf "\n%s%.3f\n\n" "packet_rate_hz = " "$packet_rate_hz"
It's a bash "one-liner". Copy and paste the whole chunk at once. Be sure to update the num_packets
variable first to how many packets you want to capture, and update your tcpdump
line to filter on just your packets of interest. In other words, modify these 2 lines for your needs:
num_packets="25"; \
time sudo tcpdump -i any -c "$num_packets" \
"(src 127.0.0.1 and port 40000) and (dst 127.0.0.1 and port 40001)"; \
It will print out the packet rate at the end, like this (where the true packet rate is 10.00 Hz):
packet_rate_hz = 9.418
Note that due to some overhead of tcpdump
as is starts up, the calculated frequency shown (9.698 Hz in this case) is skewed a bit low. I know its actual frequency is 10.00 Hz (yes, >2 zeros of precision), since I wrote the code which sends the packets and used precision timing to do so--see my answer here for the timing part of that code: How to run a high-resolution, high-precision periodic loop in Linux easily, at any frequency (ex: up to 10 KHz~100 KHz) using a soft real-time scheduler and nanosecond delays
So, for Option 1, using larger packet counts (setting num_packets
to a larger value, such as 25
to 50
or more) is better because it reduces the error introduced by the overhead of starting up tcpdump
, which we are including in our timing, but don't want to.
Example full command and output on real data:
$ num_packets="25"; \
> time_start_sec="$(date +"%s.%N")"; \
> time sudo tcpdump -i any -c "$num_packets" \
> "(src 127.0.0.1 and port 40000) and (dst 127.0.0.1 and port 40001)"; \
> time_end_sec="$(date +"%s.%N")"; \
> dt_sec="$(bc <<< "scale=20; $time_end_sec - $time_start_sec")"; \
> packet_rate_hz="$(bc <<< "scale=20; $num_packets / $dt_sec")"; \
> printf "\n%s%.3f\n" "packet_rate_hz = " "$packet_rate_hz"
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
11:58:36.181547 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:36.281798 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:36.381547 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:36.481842 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:36.581581 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:36.681724 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:36.781734 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:36.881735 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:36.981637 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:37.081633 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:37.181585 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:37.281661 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:37.381456 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:37.481732 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:37.581709 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:37.681696 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:37.781710 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:37.881603 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:37.981751 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:38.081860 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:38.181669 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:38.281727 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:38.381457 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:38.481574 IP localhost.40000 > localhost.40001: UDP, length 44
11:58:38.581724 IP localhost.40000 > localhost.40001: UDP, length 44
25 packets captured
50 packets received by filter
0 packets dropped by kernel
real 0m2.651s
user 0m0.016s
sys 0m0.017s
packet_rate_hz = 9.418
Option 2 [BEST--USE THIS WHEN POSSIBLE INSTEAD!]: extract the first and last packet timestamps from the tcpdump
output and use those to determine the packet rate
For this technique, we need to do a few things:
- First off, always use the
-l
(dash lower-case L, NOT dash one) flag with tcpdump
here to make the stdout output line buffered so that you will see it periodically update while the script is running. If you don't use this flag, you'll only see the output once at the very end of the tcpdump
command if the output is short, or in huge periodic chunks each time the stdout buffer totally fills up if the output is long.
- Pipe the output to
tee
so that it A) saves it into a file you can extract the timestamps from afterwards and B) prints it to stdout so you can see it live.
Here is the "one-liner". Note that the time
part is optional. It just prints out a total time at the end, which I think is nice. Again, update the num_packets
value and the tcpdump
command for your needs. The minimum num_packets
value you can use is 2
. This technique has such good accuracy that you do not need to use large num_packets
values to make it work well. Collecting as few as 2
packets produces good results, and 10
packets produces excellent results.
num_packets="10"; \
time sudo tcpdump -l -i any -c "$num_packets" \
"(src 127.0.0.1 and port 40000) and (dst 127.0.0.1 and port 40001)" \
| tee tcpdump.txt; \
time_start_sec="$(grep -o \
'[0-9][0-9]:[0-9][0-9]:[0-9][0-9]\.[0-9]*' \
"tcpdump.txt" \
| head -n 1 | grep -o '[0-9][0-9]\.[0-9]*')"; \
time_end_sec="$(grep -o \
'[0-9][0-9]:[0-9][0-9]:[0-9][0-9]\.[0-9]*' \
"tcpdump.txt" \
| tail -n 1 | grep -o '[0-9][0-9]\.[0-9]*')"; \
dt_sec="$(bc <<< "scale=20; $time_end_sec - $time_start_sec")"; \
packet_rate_hz="$(bc <<< "scale=20; ($num_packets-1) / $dt_sec")"; \
printf "\n%s%.4f\n\n" "packet_rate_hz = " "$packet_rate_hz"
Again, modify these 2 lines (shown below) for your needs, keeping these things in mind:
- For the
tcpdump
line, be sure to keep -l
and | tee tcpdump.txt
, as described above.
- Also, using
-t
with tcpdump
means "Don't print a timestamp on each dump line.", and if used, will cause this technique to not work since it relies on those timestamps. Option 1 above will still work just fine, however.
- If you are working on an embedded Linux board, consider the following:
- It may have a mostly read-only file-system, in which case you will need to force the
tee
output into a file in a writable part of your file system, such as perhaps /var/log/tcpdump.txt
or /tmp/tcpdump.txt
instead of just tcpdump.txt
in your current directory.
- If no part of your filesystem is writable by default, then you may need to unmount it and remount it as read-writable (
rw
) first, as shown below. Then, run the tcpdump
command above. When done, you can mount it back as read-only (ro
) as shown here:
# Remount your root dir (/) as read-writable (rw)
mount -o remount,rw /
# Remount your root dir (/) as read-only (ro)
mount -o remount,ro /
- Your embedded Linux board may not have the
sudo
command, in which case you'll need to ssh or log into the board as root directly, and also remove the sudo
word from the tcpdump
command above.
num_packets="10"; \
time sudo tcpdump -l -i any -c "$num_packets" \
"(src 127.0.0.1 and port 40000) and (dst 127.0.0.1 and port 40001)" \
| tee tcpdump.txt; \
Example packet rate printed at the end (where the true packet rate is 10.00 Hz):
packet_rate_hz = 10.0003
Example full command and output on real data. Note that when line-buffering the output with -l
, the stderr
output, such as the lines which indicate how many packets were captured, received, and dropped, get accidentally intermingled with the rest of the stdout
output. That's ok. It's non-ideal, but there's nothing we can do about it. So, it's normal.
$ num_packets="10"; \
> time sudo tcpdump -l -i any -c "$num_packets" \
> "(src 127.0.0.1 and port 40000) and (dst 127.0.0.1 and port 40001)" \
> | tee tcpdump.txt; \
> time_start_sec="$(grep -o \
> '[0-9][0-9]:[0-9][0-9]:[0-9][0-9]\.[0-9]*' \
> "tcpdump.txt" \
> | head -n 1 | grep -o '[0-9][0-9]\.[0-9]*')"; \
> time_end_sec="$(grep -o \
> '[0-9][0-9]:[0-9][0-9]:[0-9][0-9]\.[0-9]*' \
> "tcpdump.txt" \
> | tail -n 1 | grep -o '[0-9][0-9]\.[0-9]*')"; \
> dt_sec="$(bc <<< "scale=20; $time_end_sec - $time_start_sec")"; \
> packet_rate_hz="$(bc <<< "scale=20; ($num_packets-1) / $dt_sec")"; \
> printf "\n%s%.4f\n\n" "packet_rate_hz = " "$packet_rate_hz"
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
11:59:35.481669 IP localhost.40000 > localhost.40001: UDP, length 44
11:59:35.581440 IP localhost.40000 > localhost.40001: UDP, length 44
11:59:35.681468 IP localhost.40000 > localhost.40001: UDP, length 44
11:59:35.781661 IP localhost.40000 > localhost.40001: UDP, length 44
11:59:35.881649 IP localhost.40000 > localhost.40001: UDP, length 44
10 packets captured
28 packets received by filter
0 packets dropped by kernel
11:59:35.981540 IP localhost.40000 > localhost.40001: UDP, length 44
11:59:36.081669 IP localhost.40000 > localhost.40001: UDP, length 44
11:59:36.181423 IP localhost.40000 > localhost.40001: UDP, length 44
11:59:36.281595 IP localhost.40000 > localhost.40001: UDP, length 44
11:59:36.381638 IP localhost.40000 > localhost.40001: UDP, length 44
real 0m1.098s
user 0m0.021s
sys 0m0.009s
packet_rate_hz = 10.0003
Option 1 vs Option 2
If Option 2 above is so much better and produces so much better results, why would we ever use Option 1?
Well, if you ever use the -t
flag with tcpdump
, man tcpdump
indicates that it means:
-t Don't print a timestamp on each dump line.
So, Option 2 clearly doesn't work if you use that flag, since it relies on those timestamps. But, Option 1 would still work just fine there!
If any other scenarios or possible tcpdump
commands come up which don't work well with Option 2, you can use Option 1, which always works. But, whenever possible, use Option 2, as it produces much more accurate and precise output!
References
- To help me come up with my regular expressions in
grep
: https://regex101.com/