Recently I need to test whether a switch was enforcing QoS marking policies on incoming frames. To simplify lab testing, rather than reconfigure a VoIP phone I decided to generate packets with various QoS markings with scapy. In this article, we'll see how to mimic the markings typical to VoIP phones and verify that the markings remain intact on the other end using Wireshark.
There are two QoS values we need to test: IEEE 802.1Q priority marking (class of service, or CoS) and the IP differentiated services control point (DSCP) field. Both of these have a default value of zero.
>>> Dot1Q().display() ###[ 802.1Q ]### prio= 0 id= 0 vlan= 1 type= 0x0 >>> IP().display() ###[ IP ]### version= 4 ihl= None tos= 0x0 len= None id= 1 flags= frag= 0 ttl= 64 proto= ip chksum= None src= 127.0.0.1 dst= 127.0.0.1 \options\
An Ethernet frame can only be marked at layer two if it has an 802.1Q header. Packets traveling out an access port or inside the untagged native VLAN on a trunk don't receive an 802.1Q header and thus have no priority field, and must rely on layer three QoS markings.
We'll begin building our packet with an Ethernet header with a specified destination MAC address and an 802.1Q header with a VLAN ID and priority (CoS) of five:
>>> l2=Ether(dst="00:23:7d:00:d0:a8")/Dot1Q(vlan=10, prio=5)
Setting the layer three DSCP field requires a bit more forethought. When we talk about DSCP values, we're usually referring to only the first six bits of an eight-bit field. This allows for a decimal value range of 0 through 63. For example, you'll likely recognize 46 as the expedited forwarding (EF) DSCP associated with voice and other real-time traffic. A complete list of DSCP values is shown on the QoS cheat sheet.
Scapy, however, requires us to provide an eight-bit value when setting the field in our packet. How do we convert our decimal DSCP value of 46 (EF) to an eight-bit value? Find 46 in binary, add two trailing zeros, and convert it back to decimal.
46 = 101110 10111000 = 184
Now we know to set our packet's DSCP field equal to 184 decimal. Scapy displays the DSCP field as a hexadecimal value.
>>> l3=IP(dst="192.168.0.1", src="192.168.0.2", tos=184) >>> l3 <IP tos=0xb8 src=192.168.0.2 dst=192.168.0.1 |>
Now we'll top off our packet with an empty UDP payload and see what it looks like glued together.
>>> l4=UDP() >>> l2/l3/l4 <Ether dst=00:23:7d:00:d0:a8 type=0x8100 |>>>
Looks good! Now we can send it out on the wire. Remember that since we're working at layer two, we need to use the sendp() function, not send(), to transmit our packet. Note that scapy requires root or administrator privileges to send raw packets.
>>> sendp(l2/l3/l4, iface="eth1") . Sent 1 packets.
We can use Wireshark to verify that our packet has been marked with the appropriate CoS and DSCP values at the receiving end.
One last note: If playing along at home, you may notice that Wireshark marks our packet as malformed. This is due to what it perceives as an erroneous (missing) DNS message in the UDP payload of the packet and can be safely ignored.