Subject: Re: [net] AF_PACKET PACKET_VNET_HDR CHECKSUM_PARTIAL packets bypass ct invalid classification

From: Willem de Bruijn <[email-masked]>
Date: Tue, 19 May 2026 21:29:23 -0400

Federico Brasili wrote:
> Hello,
> 
> I would like to ask for feedback on a possible checksum/conntrack inconsistency in the AF_PACKET PACKET_VNET_HDR transmit path.
> 
> A locally injected IPv4/UDP packet with an invalid raw UDP checksum is classified as ct state invalid when sent as a normal AF_PACKET raw frame. However, an otherwise equivalent packet sent through AF_PACKET with PACKET_VNET_HDR and VIRTIO_NET_HDR_F_NEEDS_CSUM is not classified as invalid and is delivered to a UDP socket, even though packet sockets still observe the UDP checksum field unchanged and report CSUMNOTREADY.
> 
> Minimal behavior observed:
> 
> 1. RAW_BAD
> 
> AF_PACKET raw frame
> UDP checksum field: 0x1111
> 
> nft:
> ct state invalid counter packets 1 drop
> udp dport 12345 counter packets 0 accept
> 
> UDP socket:
> no packet received
> 
> 2. VNET_BAD
> 
> AF_PACKET + PACKET_VNET_HDR
> VIRTIO_NET_HDR_F_NEEDS_CSUM
> csum_start = 34
> csum_offset = 6
> UDP checksum field: 0x1111
> 
> packet socket:
> PACKET_AUXDATA reports CSUMNOTREADY
> UDP header still contains checksum 0x1111
> 
> nft:
> ct state invalid counter packets 0 drop
> udp dport 12345 counter packets 1 accept
> 
> UDP socket:
> packet received
> 
> A trace of the VNET case shows the packet being converted to CHECKSUM_PARTIAL and reaching conntrack/UDP in that state:
> 
> skb_partial_csum_set(... arg_start=34 arg_off=6) = 1
> XMIT      ip_summed=3 csum_start=36 csum_offset=6
> NF_CT_UDP ip_summed=3 csum_start=36 csum_offset=6
> UDP_RCV   ip_summed=3 csum_start=36 csum_offset=6
> UDP_QUEUE ip_summed=3 csum_start=36 csum_offset=6
> 
> The relevant path appears to be:
> 
> net/packet/af_packet.c
>   packet_snd()
>   tpacket_snd()
>   __packet_snd_vnet_parse()
>   virtio_net_hdr_to_skb()
> 
> include/linux/virtio_net.h
>   __virtio_net_hdr_to_skb()
>   skb_partial_csum_set()
> 
> The same behavior was also reproduced through PACKET_TX_RING + PACKET_VNET_HDR.
> 
> An explicit nftables rule such as udp dport 12345 drop still works correctly, so this is not a general firewall bypass. The observed difference is specifically around checksum-invalid classification: raw invalid packets are treated as ct state invalid, while PACKET_VNET_HDR/NEEDS_CSUM packets with the same invalid raw checksum are not.
> 
> My question is whether this is considered intended behavior for locally injected CHECKSUM_PARTIAL skbs, or whether AF_PACKET should reject or normalize this case before the packet reaches conntrack/UDP.

This is expected.

The VIRTIO_NET_HDR_F_NEEDS_CSUM flag on transmit indicates that a
checksum hardware offload (CHECKSUM_PARTIAL) is to be programmed.
The sender will include checksum start and offset instructions.

Handling of CHECKSUM_PARTIAL skbuffs inside the kernel is described at
the top of skbuff.h. Note the section on receive processing, the point
about "are considered verified":

 * - %CHECKSUM_PARTIAL
 *
 *   A checksum is set up to be offloaded to a device as described in the
 *   output description for CHECKSUM_PARTIAL. This may occur on a packet
 *   received directly from another Linux OS, e.g., a virtualized Linux kernel
 *   on the same host, or it may be set in the input path in GRO or remote
 *   checksum offload. For the purposes of checksum verification, the checksum
 *   referred to by skb->csum_start + skb->csum_offset and any preceding
 *   checksums in the packet are considered verified. Any checksums in the
 *   packet that are after the checksum being offloaded are not considered to
 *   be verified.