slackhq/nebula · error

bad IPv4 IHL: %d

Error message

bad IPv4 IHL: %d

What it means

baseIPv4HdrSum folds the IPv4 header checksum over fields that stay constant across TCP/UDP segments. Before doing so it derives the Internet Header Length (IHL) from the first byte (low nibble * 4) and validates it against ipv4HeaderMinLen (20) and csumStart (the L3 header length). If IHL is out of that range the packet is malformed, so it refuses to compute a bogus checksum and returns this error.

Source

Thrown at overlay/tio/virtio/segment_linux.go:183

	return n
}

// basePseudoSum folds the part of the L4 pseudo-header sum that is identical
// for every segment: the source and destination addresses plus the protocol
// number. The per-segment L4 length is added by the caller inside the loop.
func basePseudoSum(pkt []byte, isV4 bool, proto uint32) uint32 {
	if isV4 {
		return uint32(checksum.Checksum(pkt[ipv4SrcOff:ipv4AddrsEnd], 0)) + proto
	}
	return uint32(checksum.Checksum(pkt[ipv6SrcOff:ipv6AddrsEnd], 0)) + proto
}

// baseIPv4HdrSum folds the IPv4 header checksum over the fields that stay constant across segments.
// csumStart is the L3 header length, which bounds a valid IHL.
func baseIPv4HdrSum(pkt []byte, csumStart int) (uint32, error) {
	ihl := int(pkt[0]&0x0f) * 4
	if ihl < ipv4HeaderMinLen || ihl > csumStart {
		return 0, fmt.Errorf("bad IPv4 IHL: %d", ihl)
	}
	// total_len, the ID, and the checksum field itself are excluded: all three are rewritten per segment.
	sum := uint32(checksum.Checksum(pkt[:ihl], 0))
	sum += uint32(^binary.BigEndian.Uint16(pkt[ipv4TotalLenOff : ipv4TotalLenOff+2]))
	sum += uint32(^binary.BigEndian.Uint16(pkt[ipv4ChecksumOff : ipv4ChecksumOff+2]))
	sum += uint32(^binary.BigEndian.Uint16(pkt[ipv4IDOff : ipv4IDOff+2]))
	sum = (sum & 0xffff) + (sum >> 16)
	sum = (sum & 0xffff) + (sum >> 16)
	return sum, nil
}

// baseTCPHdrSum folds the TCP header checksum over everything the segment loop does not rewrite
func baseTCPHdrSum(pkt []byte, csumStart, headerLen int) uint32 {
	seq := binary.BigEndian.Uint32(pkt[csumStart+tcpSeqOff : csumStart+tcpSeqOff+4])
	flags := uint16(pkt[csumStart+tcpFlagsOff])

	sum := uint32(checksum.Checksum(pkt[csumStart:headerLen], 0))
	sum += uint32(^uint16(seq >> 16))

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. Verify the buffer starts at the IPv4 header and that pkt[0]>>4 == 4 before segmenting.
  2. Check the csumStart/hdrLen values you pass to SegmentTCP/SegmentUDP: csumStart must be >= the actual IHL-derived header length.
  3. Dump the first 20 bytes of the packet; if the low nibble of byte 0 is < 5 the packet is truncated or misaligned — fix the upstream frame source (tap/virtio queue) instead of the library call.
  4. Ensure you pass the whole superpacket (starting at L3), not a slice that skips leading bytes.

Example fix

// before: csumStart computed from a fixed guess
seg.SegmentTCP(pkt, hdrLen, 14, gsoSize, yield) // 14 assumed ethernet+ VLAN mismatch
// after: derive csumStart from the actual L3 header
if pkt[0]>>4 != 4 { return errors.New("not IPv4") }
csumStart := int(pkt[0]&0x0f) * 4
seg.SegmentTCP(pkt, hdrLen, uint16(csumStart), gsoSize, yield)
Defensive patterns

Strategy: validation

Validate before calling

func validIPv4(pkt []byte, csumStart int) bool {
    if len(pkt) < 20 || pkt[0]>>4 != 4 { return false }
    ihl := int(pkt[0]&0x0f) * 4
    return ihl >= 20 && ihl <= csumStart
}
// if !validIPv4(pkt, csumStart) { skip/error before calling SegmentTCP/SegmentUDP }

Type guard

func isIPv4(pkt []byte) bool { return len(pkt) >= 20 && pkt[0]>>4 == 4 }

Try / catch

segs, err := segmenter.SegmentTCP(pkt, hdrLen, csumStart, gsoSize, yield)
if err != nil {
    var badIHL bool = strings.Contains(err.Error(), "bad IPv4 IHL")
    if badIHL { log.Warn("dropping malformed IPv4 frame"); return nil /* skip packet */ }
    return err
}

Prevention

When it happens

Trigger: Calling SegmentTCP or SegmentUDP (directly or via SegmentSuperpacket/collectTCP/collectUDP) on a buffer whose first byte's low nibble encodes IHL < 5 (i.e. header < 20 bytes) or an IHL larger than the supplied csumStart value — e.g. the packet is not actually an IPv4 header, the buffer is offset/misaligned, or the caller passed a csumStart smaller than the real header length.

Common situations: Packets captured or crafted with truncated/corrupt L3 headers; a virtio/netmap header (hdrLenU/csumStartU) that disagrees with the actual packet bytes; feeding a non-IPv4 frame (e.g. ARP, or an IPv6 packet misdetected) into the IPv4 checksum path; off-by-N offsets after manual packet parsing.

Related errors


AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03). Data as JSON: /api/errors/7651e9ecd8225f62. Report an issue: GitHub.