The claim that a VPN packet carries exactly two checksums sounds tidy and is wrong often enough to be worth unpacking. The count depends on which IP versions appear, which transport protocol is carried, and which protection format the tunnel uses. A header checksum, an upper-layer checksum and a cryptographic integrity tag are three mechanisms with three scopes. Counting them starts with marking what each one covers.

Check which IP versions are actually on the wire

IPv4 headers carry a 16-bit header checksum, and IPv6 headers carry nothing of the kind. That single fact changes the count for every tunnel, because wrapping IPv4 inside IPv6 and wrapping IPv6 inside IPv4 produce different documents. A tunnel can carry both families at once, in which case one direction may contain a field the other lacks. Establish the versions before counting anything at all.

Write the layout as a header list rather than a picture. The order matters when reading a capture, and the presence or absence of the checksum field is the first thing to mark.

outer IPv6 | ESP | inner IPv6 | UDP | application data
outer IPv4 | ESP | inner IPv4 | TCP | application data

The first line has no IP header checksum anywhere in its IP headers, while the second has two when both headers are IPv4. Same tunnel technology, different count, and neither line is a complete wire format.

Check what each checksum covers

Coverage is the part people skip, and it is what makes a count meaningful. The IPv4 header checksum protects the header and nothing else, so it says nothing about the payload behind it. A UDP checksum over IPv6 covers a pseudo-header plus the UDP header and payload, which means it reaches into the carried packet when the datagram is tunnelled. TCP has its own checksum with a similar pseudo-header dependency. Those are three different scopes even though all three are called checksums.

RFC 791 defines the IPv4 header check and its recomputation on each hop, and RFC 8200 defines the upper-layer treatment over IPv6 including the pseudo-header. Neither document says a VPN packet holds a fixed number of checksums, because neither is describing a tunnel.

tshark -r capture.pcap -Y "ip.proto == 50" -V | grep -i checksum

Check the integrity protection separately

ESP integrity is cryptographic protection with its own coverage and an algorithm-dependent representation. It is not an IP or UDP checksum under another name, and its length varies with the chosen construction. An integrity check covers a defined portion of the packet, which may include the ESP header and part of the payload depending on the mode in use. Counting it as a checksum field of known width is where most of these explanations fall apart.

Read the selected construction before counting bytes, and describe the result as integrity protection rather than as a checksum with a different label. The decoder will name the field for you if you ask it.

tshark -r capture.pcap -Y esp -V | grep -iE "integrity|icv"

Check the observation point before counting

An inner-interface capture and a wire capture can show completely different layers. On the inner interface the tunnel is already stripped, so you see the carried packet and none of the outer headers. On the wire you see the outer headers and an encrypted payload you cannot read. Neither view is wrong, and they are not views of the same object. State where the capture was taken before quoting any count.

sudo tcpdump -ni eth0 -vv -c 5 esp
sudo tcpdump -ni tun0 -vv -c 5

The first command shows the outer packet with its integrity trailer, and the second shows the carried packet with its own transport checksum. Adding numbers from both captures does not double-count anything, but it does explain the same packet twice.

Check the count against a real capture

Decode one packet fully and mark every field the decoder labels as a checksum or an integrity value. Then compare each mark against the layout you wrote, and delete any that belongs to a layer you are not actually counting. A count that survives that exercise is defensible, and a count quoted from memory is not.

Repeat for the reply direction, because a tunnel carrying both families can differ per direction and per flow. Then state the count with its scope attached, for example three checksums for this inner packet at this observation point, rather than as a property of VPN packets in general.

tshark -r capture.pcap -T fields -e ip.checksum -e udp.checksum -e tcp.checksum

Makomi ya ndenge moko