QoS troubleshooting has a major visibility problem.
Reading QoS Markings Off the Wire with dscpdump
Your firewall or SD-WAN policy says traffic should be marked AF41. Your application team says the WAN is dropping their video calls. The network team says QoS is configured correctly. Nobody is actually looking at what’s on the wire.
dscpdump fixes that. It’s a GNU awk script that wraps tcpdump and decodes DSCP, ECN, and legacy ToS markings in real time, one line per packet. No Wireshark, no capture files, no post-processing. Best of all: No Math For You!
Extra bonus: No Python interpreter-of-the-day and zero dependendencies. Who doesn’t have GNU awk these days? (ok, my Mac didn’t, but Homebrew hooked me up).
Sample output:
DF TCP 192.0.2.210 44948 10.27.0.192 22 [S]
AF22 TCP 10.27.0.192 22 192.0.2.210 44948 [S.]
DF TCP 192.0.2.210 44948 10.27.0.192 22 [.]
AF22 TCP 10.27.0.192 22 192.0.2.210 44948 [P.]
DF TCP 192.0.2.210 44948 10.27.0.192 22 [.]
DF TCP 192.0.2.210 44948 10.27.0.192 22 [P.]
DF TCP 192.0.2.210 44948 10.27.0.192 22 [F.]
AF22 TCP 10.27.0.192 22 192.0.2.210 44948 [.]
AF22 TCP 10.27.0.192 22 192.0.2.210 44948 [P.]
AF22 TCP 192.0.2.210 44948 10.27.0.192 22 [R]
DF UDP 192.0.2.210 54389 192.0.2.162 161
CS2 UDP 192.0.2.162 161 192.0.2.210 54389
DF UDP 192.0.2.210 54389 192.0.2.162 161
CS2 UDP 192.0.2.162 161 192.0.2.210 54389
DF ICMP 192.0.2.210 10.27.0.42
CS2 ICMP 10.27.0.42 192.0.2.210
DF UDP 192.0.2.210 33195 192.0.2.92 161
CS2 UDP 192.0.2.92 161 192.0.2.210 33195
DF UDP 192.0.2.210 33195 192.0.2.92 161
CS2 UDP 192.0.2.92 161 192.0.2.210 33195
DF TCP 192.0.2.214 54474 192.0.2.210 443 [S]
DF TCP 192.0.2.210 443 192.0.2.214 54474 [S.]
DF TCP 192.0.2.214 54474 192.0.2.210 443 [.]
DF TCP 192.0.2.214 54474 192.0.2.210 443 [P.]
How To Get It
The script is available in my network-tools repo.
git clone https://github.com/duanetoler/network-tools
Copy it to somewhere usable when you run sudo for tcpdump. Your home directory may or may not work, such as if your home directory is NFS-mounted.
sudo cp bin/dscpdump /usr/local/bin
Background
The IPv4 ToS byte has had two lives. Originally, it carried IP Precedence in the top 3 bits and four ToS flags (Delay, Throughput, Reliability, and a reserved bit) in the next four. RFC 2474 repurposed the top 6 bits as the Differentiated Services Code Point (DSCP), leaving the bottom 2 bits for ECN per RFC 3168.
The byte layout:
7 6 5 4 3 2 1 0
[ DSCP (6 bits) ][ ECN ]
DSCP values you’ll see in practice:
- DF (0) — Default Forwarding, best effort
- CS1–CS7 — Class Selector, backward compatible with IP Precedence
- AF11–AF43 — Assured Forwarding, four classes with three drop precedences each
- EF (46) — Expedited Forwarding, the voice/low-latency queue
- Legacy ToS — Pre-DSCP markings, increasingly rare but still out there
ECN values:
- 00 — Not ECN-capable
- 01/10 — ECT(1)/ECT(0), sender indicates ECN support
- 11 — CE (Congestion Experienced), router marked instead of dropped
For the recently-initiated: ECN is Explicit Congestion Notification. This was a sort of adaptation of congestion notification already present it older WAN protocols such as ATM and Frame-Relay. ECN is congestion notification for TCP layer 4 and IP layer 3 protocols.
ECT is “ECN Capable Transport”, a device that “speaks” ECN.
How The Script Works
tcpdump with -v splits each IP packet across two lines — the IP header on the first, the payload summary on the second.
The script expects the string tos to be present in Field 3 of the output to identify header lines, extracts the ToS byte from Field 4, then calls getline to advance to the payload line for the address and protocol fields.
The ToS byte extraction is straightforward bitwise math:
ecn = and(tos, 3); # mask bottom 2 bits
dscp = rshift(and(tos, 252), 2); # mask top 6, shift to 0-63
cs = rshift(dscp, 3); # class selector (top 3 of DSCP)
dp = and(dscp, 3); # drop precedence (bottom 2 of DSCP... wait
One subtlety: for AF codepoints, drop precedence lives in bits 1-2 of the DSCP field (the bottom 2 of the top 6), not the bottom 2 of the full byte. and(dscp, 3) after the right-shift gets you there cleanly.
The legacy ToS branch (dscp < 8) handles pre-RFC 2474 markings by inspecting the original flag bits directly in the raw ToS byte. These show up occasionally on older equipment or misconfigured hosts and are worth identifying rather than silently misclassifying.
“It’s just that easy!”
Usage
sudo tcpdump -lvnni eth0 ip | ./dscpdump
Filter to a specific host:
sudo tcpdump -lvnni eth0 ip and host 192.0.2.1 | ./dscpdump
Pass -v DEBUG=1 on the command line to see the raw decoded values alongside each packet. Here you can see how tos/dscp/cs interrelate. See? Two lives of ToS!
tos: 0; dscp: 0; cs: 0; dp: 0; ecn: 0
DF ICMP 192.0.2.210 192.0.2.178
tos: 64; dscp: 16; cs: 2; dp: 0; ecn: 0
CS2 ICMP 192.0.2.178 192.0.2.210
tos: 0; dscp: 0; cs: 0; dp: 0; ecn: 0
DF ICMP 192.0.2.210 192.0.2.216
tos: 64; dscp: 16; cs: 2; dp: 0; ecn: 0
CS2 ICMP 192.0.2.216 192.0.2.210
Requirements
- gawk (GNU awk). The script uses
and(),rshift(), andstrtonum(), which are GNU extensions not available in POSIX awk. - For macOS 11+, you will need the Homebrew package to get GNU awk. You MUST edit the first line of the script to specify the path to
gawk: It’s usually/usr/local/bin/gawk -f(that’s where it is on my Mac) - tcpdump with
-vand-lflags. Field positions are fixed to this invocation; omitting either flag produces unparseable output.