slackhq/nebula · error

unable to determine IP version from packet

Error message

unable to determine IP version from packet

What it means

The tun device write path (tun.Write on FreeBSD) prepends a 4-byte BSD header whose byte 3 encodes the address family (AF_INET or AF_INET6). The first byte of the outgoing packet is inspected to determine the IP version; if it is neither 4 nor 6 the packet is malformed and the write fails with this error. The library refuses to guess the family because writing it into the tun header incorrectly would corrupt routing on the FreeBSD tun interface.

Source

Thrown at overlay/tun_freebsd.go:205

	}
}

// Write is only valid for single threaded use
func (t *tun) Write(from []byte) (int, error) {
	if len(from) <= 1 {
		return 0, syscall.EIO
	}

	ipVer := from[0] >> 4
	var head [4]byte
	// first 4 bytes is protocol family, in network byte order
	switch ipVer {
	case 4:
		head[3] = syscall.AF_INET
	case 6:
		head[3] = syscall.AF_INET6
	default:
		return 0, fmt.Errorf("unable to determine IP version from packet")
	}

	iovecs := [2]syscall.Iovec{
		{&head[0], 4},
		{&from[0], uint64(len(from))},
	}
	for {
		n, _, errno := syscall.Syscall(syscall.SYS_WRITEV, uintptr(t.fd), uintptr(unsafe.Pointer(&iovecs[0])), 2)
		if errno == 0 {
			return int(n) - 4, nil
		}
		switch errno {
		case unix.EAGAIN:
			if err := t.blockOnWrite(); err != nil {
				return 0, err
			}
		case unix.EINTR:
			// retry

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. Verify the packet buffer begins with a valid IP version nibble before calling Write: (b[0]>>4) must equal 4 or 6
  2. If your source traffic is Ethernet-framed, strip the 14-byte Ethernet header and pass only the IP payload
  3. Check upstream code for off-by-one slicing that shifts the IP header out of position
  4. Drop the malformed packet and log it instead of propagating an error into the write loop

Example fix

// before
_, err := tunDev.Write(buf)
// after
if len(buf) == 0 || (buf[0]>>4 != 4 && buf[0]>>4 != 6) {
    log.Warn("dropping non-IP packet", "firstByte", buf)
    continue
}
_, err := tunDev.Write(buf)
Defensive patterns

Strategy: validation

Validate before calling

func isValidIPPacket(b []byte) bool {
    return len(b) >= 1 && (b[0]>>4 == 4 || b[0]>>4 == 6)
}
// call site:
if !isValidIPPacket(buf) { log.Warn("drop non-IP packet"); continue }

Type guard

func isIPv4(b []byte) bool { return len(b) >= 20 && b[0]>>4 == 4 }
func isIPv6(b []byte) bool { return len(b) >= 40 && b[0]>>4 == 6 }

Try / catch

n, err := tunDev.Write(buf)
if err != nil {
    if strings.Contains(err.Error(), "unable to determine IP version") {
        log.Warn("skipping malformed packet", "err", err)
        continue
    }
    return err
}

Prevention

When it happens

Trigger: Calling Write() on the FreeBSD tun with a buffer that does not begin with a valid IP header — the first nibble of the first byte must be 0x4 (IPv4) or 0x6 (IPv6). This happens with zero-length packets, packets containing Ethernet frames instead of raw IP, or corrupted/partial buffers.

Common situations: Passing L2 (Ethernet-framed) traffic from a layer-2 VPN mode into an L3 tun; reading packets from a socket and slicing off too many header bytes; a peer sending non-IP garbage over a raw channel; an empty or uninitialized buffer after a failed read upstream.

Related errors


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